20145211 《信息安全系统设计基础》第十一周学习总结
20145211 《信息安全系统设计基础》第十一周学习总结——书不尽言,言不尽意
教材学习内容总结
异常
- 异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是有操作系统实现的。
- 异常:控制流中的突变,用来响应处理器状态中的某些变化。
在处理器中,状态被编码为不同的位和信号。状态变化成为事件。
异常表:当处理器监测到有时间发生时,通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。 - 当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况的一种:
- 处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令。
处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令。
处理程序终止被中断的程序。 - 异常号:到异常表中的索引
- 异常表基址寄存器:异常表的起始地址存放的位置。
- 异常与过程调用的异同:
- 过程调用时,在跳转到处理器之前,处理器将返回地址压入栈中。然而,- 根据异常的类型,返回地址要么是当前指令,要么是下一条指令。
- 处理器把一些额外的处理器状态压入栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。
如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核栈中,而不是压到用户栈中。 - 异常处理程序运行在内核模式下,意味着它们对所有的系统资源都有完全的访问权限。
异常的类别
- 异常的分类:中断、陷阱、故障和终止。
- 中断:异步发生,是来自处理器外部的I/O设备的信号的结果。 硬件异常中断处理程序通常称为中断处理程序。
- 陷阱和系统调用:
- 陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
- 普通的函数运行在用户模式中,用户模式限制了函数可以执行的指令的类型,而且它们只能访问与调用函数相同的栈。系统调用运行在内核模式中,内核模式允许系统调用执行指令,并访问定义在内核中的栈。
- 故障:是由错误情况引起的。
- 终止:是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制返回给应用程序。
- 031号:由intel架构师定义的异常;32255号:操作系统定义的中断和陷阱。
- 每一个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。
进程
- 异常是允许操作系统提供进程的概念所需要的基本构造块。
- 进程:一个执行中的程序的实例。
- 上下文是由程序正确运行所需要的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
- 进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,独占地使用处理器;
- 一个私有的地址空间,独占地使用存储器系统。
用户模式和内核模式
- 模式位:用某个控制寄存器中的一个位模式,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
- 当设置了位模式,进程就运行在内核模式中,一个运行在内核模式中的进程可以中兴指令集中的任何指令,而且可以访问系统中任何存储器位置。
没有设置位模式时,进程就运行在用户模式中,不允许执行特权指令,例如停止处理器、改变位模式,或者发起一个I/O操作。 - 用户程序必须通过系统调用接口间接的当问内核代码和数据。
- 进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、或者陷入系统调用这样的异常。
上下文切换
- 上下文就是内核重新启动一个被抢占的进程所需的状态。
调度:内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。有内核中称为调度器的代码处理的。
系统调用错误处理
- 错误处理包装函数:包装函数调用基本函数,检查错误,如果有任何问题就终止。
进程控制
获取进程ID
- 每个进程都有一个唯一的正数的进程ID。
getpid函数返回调用进程的PID,getppid函数返回它的父进程的PID。上面两个函数返回一个同类型为pid_t的整数值,在linux系统中,它在types.h中被定义为int。 - 创建和终止进程
进程总处于三种状态
运行:进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
停止:程序的执行被挂起,,且不会被调度。
终止:进程用永远停止了。终止原因:(1)收到一个信号,默认行为是终止进程;(2)从主进程返回(3)调用exit函数。
回收子进程
- 当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。
一个终止了但还未被回收的进程称为僵死进程。
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
//返回:若成功,返回子进程的PID;若WNOHANG,返回0;若其他错误,返回-1
- 修改默认行为,通过options设置:
WNOHANG:默认行为是挂起调用进程。
WUNTRACED:默认行为是只返回已终止的子进程。
WNOHANG|WUNTRACED:立即返回,如果等待集合中没有任何子进程被停止或者已终止,那么返回值为0,或者返回值等于那个被停止或者已经终止的子进程的PID。
错误条件:
若调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD;
若waitpid函数被一个信号中断,那么返回-1,并设置errno为EINTR
信号
信号术语
-
发送信号的两个不同步骤:
-
发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。
-
接收信号:信号处理程序捕获信号的基本思想。
-
当内核从一个异常处理程序返回,准备将控制传递该进程p时,它会检查进程p的未被阻塞的待处理信号的集合。如果这个集合是非空的,那么内核选择集合中的某个信号k,并且强制p接收信号k。
进程可以通过使用signal函数修改和信号相关联的默认行为。 唯一例外是SIGSTOP和SIGKILL,它们的默认行为是不能被修改的。#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum,sighandler_t handler);
//返回:若成功,返回指向前次处理程序的指针;若出错,为SIG_ERR
##实践部分
###exec1.c
程序代码
include <stdio.h>
include <unistd.h>
int main()
{
char *arglist[3];
arglist[0] = "ls";
arglist[1] = "-l";
arglist[2] = 0 ;//NULL
printf("* * * About to exec ls -l\n");
execvp( "ls" , arglist );//第一个参数传递的是文件名
printf("* * * ls is done. bye");
return 0;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127220607784-376890594.png)
- execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。
在执行时exevp函数调用成功没有返回,所以没有打印“* * * ls is done. bye”
###exec2.c
程序代码
include <stdio.h>
include <unistd.h>
int main(){
char arglist[3];
arglist[0] = "ls";
arglist[1] = "-l";
arglist[2] = 0 ;
printf(" * * About to exec ls -l\n");
execvp( arglist[0] , arglist );
printf("* * * ls is done. bye\n");
}
- exec2把“ls”替换成了“arglist[0]”,所以并不会影响结果。
###exec3.c
程序代码
include <stdio.h>
include <unistd.h>
int main(){
char arglist[3];
charmyenv[3];
myenv[0] = "PATH=:/bin:";
myenv[1] = NULL;
arglist[0] = "ls";
arglist[1] = "-l";
arglist[2] = 0 ;
printf("* * * About to exec ls -l\n");
execlp("ls", "ls", "-l", NULL);
printf("* * * ls is done. bye\n");
}
- int execlp(const char * file,const char * arg,....);
execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。
指定了环境变量,然后依然执行了ls -l指令,成功后没有返回,所以最后一句话不会输出。运行结果同exec1。
exec函数族
fork()函数通过系统调用创建一个与原来进程(父进程)几乎完全相同的进程,在fork后的子进程中使用exec函数族,可以装入和运行其它程序(子进程替换原有进程,和父进程做不同的事),fork创建一个新的进程就产生了一个新的PID,exec启动一个新程序,替换原有的进程,因此这个新的被exec执行的进程的PID不会改变。
exec函数族装入并运行程序path/file,并将参数arg0(arg1, arg2, argv[], envp[])传递给子程序,出错返回-1。
在exec函数族中,后缀l、v、p、e指定函数将具有某种操作能力:
###forkdemo1.c
程序代码
include <stdio.h>
include<sys/types.h>
include<unistd.h>
int main(){
int ret_from_fork, mypid;
mypid = getpid();
printf("Before: my pid is %d\n", mypid);
ret_from_fork = fork();
sleep(1);
printf("After: my pid is %d, fork() said %d\n",
getpid(), ret_from_fork);
return 0;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127220801268-612704890.png)
- 这个代码先是打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0。
父进程通过调用fork函数创建一个新的运行子进程。
调用一次,返回两次。一次返回到父进程,一次返回到新创建的子进程。
###forkdemo2.c
程序代码
include <stdio.h>
include <unistd.h>
int main()
{
printf("before:my pid is %d\n", getpid() );
fork();
fork();
printf("aftre:my pid is %d\n", getpid() );
return 0;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127220821175-597165530.png)
- 这个代码调用两次fork,一共产生四个子进程,所以会打印四个aftre输出。
###forkdemo3.c
程序代码
include <stdio.h>
include <stdlib.h>
include <unistd.h>
int fork_rv;
int main()
{
printf("Before: my pid is %d\n", getpid());
fork_rv = fork(); /* create new process */
if ( fork_rv == -1 ) /* check for error */
perror("fork");
else if ( fork_rv == 0 ){
printf("I am the parent. my child is %d\n", getpid());
exit(0);
}
else{
printf("I am the parent. my child is %d\n", fork_rv);
exit(0);
}
return 0;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127220845893-600997818.png)
- fork函数会将一个进程分成两个进程,并且会返回两次,所以如上图所示,我们可以看到,出现了一次“I am the parent. my child is 4954”,又出现了一次“I am the parent. my child is 4954”。
这个代码进行了错误处理,提高了代码的健壮性。
###forkdemo4.c
程序代码
include <stdio.h>
include <stdlib.h>
include <unistd.h>
int main(){
int fork_rv;
printf("Before: my pid is %d\n", getpid());
fork_rv = fork(); /* create new process /
if ( fork_rv == -1 ) / check for error */
perror("fork");
else if ( fork_rv == 0 ){
printf("I am the child. my pid=%d\n", getpid());
printf("parent pid= %d, my pid=%d\n", getppid(), getpid());
exit(0);
}
else{
printf("I am the parent. my child is %d\n", fork_rv);
sleep(10);
exit(0);
}
return 0;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127220858581-561933400.png)
- 先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,执行sleep(10)语句,休眠十秒。
子进程返回0,所以输出child与之后一句。
###forkgdb.c
程序代码
include <stdio.h>
include <stdlib.h>
include <unistd.h>
int gi=0;
int main()
{
int li=0;
static int si=0;
int i=0;
pid_t pid = fork();
if(pid == -1){
exit(-1);
}
else if(pid == 0){
for(i=0; i<5; i++){
printf("child li:%d\n", li++);
sleep(1);
printf("child gi:%d\n", gi++);
printf("child si:%d\n", si++);
}
exit(0);
}
else{
for(i=0; i<5; i++){
printf("parent li:%d\n", li++);
printf("parent gi:%d\n", gi++);
sleep(1);
printf("parent si:%d\n", si++);
}
exit(0);
}
return 0;
}
运行结果
- 父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰。
###psh1.c
程序代码
include <stdio.h>
include <stdlib.h>
include <string.h>
include <unistd.h>
define MAXARGS 20
define ARGLEN 100
int execute( char *arglist[] )
{
execvp(arglist[0], arglist);
perror("execvp failed");
exit(1);
}
char * makestring( char *buf )
{
char *cp;
buf[strlen(buf)-1] = '\0';
cp = malloc( strlen(buf)+1 );
if ( cp == NULL ){
fprintf(stderr,"no memory\n");
exit(1);
}
strcpy(cp, buf);
return cp;
}
int main()
{
char *arglist[MAXARGS+1];
int numargs;
char argbuf[ARGLEN];
numargs = 0;
while ( numargs < MAXARGS )
{
printf("Arg[%d]? ", numargs);
if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n' )
arglist[numargs++] = makestring(argbuf);
else
{
if ( numargs > 0 ){
arglist[numargs]=NULL;
execute( arglist );
numargs = 0;
}
}
}
return 0;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127220931190-1502802910.png)
- 依次你输入要执行的指令与参数,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令。
第一个是程序名,然后依次是程序参数。
一个字符串,一个字符串构造参数列表argist,最后在数组末尾加上NULL
将arglist[0]和arglist数组传给execvp。
程序正常运行,execvp命令指定的程序代码覆盖了shell程序代码,并在命令结束之后退出,shell就不能再接受新的命令。
###psh2.c
程序代码
include <stdio.h>
include <stdlib.h>
include <string.h>
include <sys/types.h>
include <sys/wait.h>
include <unistd.h>
include <signal.h>
define MAXARGS 20
define ARGLEN 100
char *makestring( char *buf )
{
char *cp;
buf[strlen(buf)-1] = '\0';
cp = malloc( strlen(buf)+1 );
if ( cp == NULL ){
fprintf(stderr,"no memory\n");
exit(1);
}
strcpy(cp, buf);
return cp;
}
void execute( char *arglist[] )
{
int pid,exitstatus;
pid = fork();
switch( pid ){
case -1:
perror("fork failed");
exit(1);
case 0:
execvp(arglist[0], arglist);
perror("execvp failed");
exit(1);
default:
while( wait(&exitstatus) != pid )
;
printf("child exited with status %d,%d\n",
exitstatus>>8, exitstatus&0377);
}
}
int main()
{
char *arglist[MAXARGS+1];
int numargs;
char argbuf[ARGLEN];
numargs = 0;
while ( numargs < MAXARGS )
{
printf("Arg[%d]? ", numargs);
if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n' )
arglist[numargs++] = makestring(argbuf);
else
{
if ( numargs > 0 ){
arglist[numargs]=NULL;
execute( arglist );
numargs = 0;
}
}
}
return 0;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221010175-1128507812.png)
- 功能:在子进程中执行用户输入的指令,利用wait函数,通过父进程,实现循环输入指令。
这个代码与psh1.c代码最大的区别就在于execute函数。 调用wait(&status)等价于调用waitpid(-1.&status,0),当option=0时,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。只要有一个子进程没有结束,父进程就被挂起。所以当wait返回pid时没说明,子进程都已经结束,即用户输入的指令都已经执行完毕。因为execute函数在大的循环中调用,所以会循环执行下去,除非用户强制退出。
另外,当子进程正常执行完用户指令后,子进程的状态为0,若执行指令出错,子进程的状态为1。
###testbuf1.c
程序代码
include <stdio.h>
include <stdlib.h>
int main()
{
printf("hello");
fflush(stdout);
while(1);
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221117081-925278280.png)
- 效果是先输出hello,然后保持在循环中不结束进程。
###testbuf2.c
程序代码
include <stdio.h>
int main()
{
printf("hello\n");
while(1);
}
运行结果
和testbuf1.c运行结果一样
- 从testbuf1.c和testbuf2.c的运行结果上看,我们可以猜出fflush(stdout);的功能就是打印换行符。
###testbuf3.c
程序代码
include <stdio.h>
int main()
{
fprintf(stdout, "1234", 5);
fprintf(stderr, "abcd", 4);
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221128925-2013361608.png)
- 将内容格式化输出到标准错误、输出流中。
###testpid.c
程序代码
include <stdio.h>
include <unistd.h>
include <sys/types.h>
int main()
{
printf("my pid: %d \n", getpid());
printf("my parent's pid: %d \n", getppid());
return 0;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221150893-290820704.png)
- 功能:输出当前进程pid和当前进程的父进程的pid。
###testsystem.c
程序代码
include <stdlib.h>
int main ( int argc, char *argv[] )
{
system(argv[1]);
system(argv[2]);
return EXIT_SUCCESS;
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221203315-1651743119.png)
- system函数:发出一个DOS命令
用法: int system(char *command);
system函数需加头文件
###waitdemo2.c
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221255675-1886825165.png)
- 多了一个子进程的状态区分,把状态拆分成三块,exit,sig和core。
###environ.c
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221308690-1617567340.png)
功能:打印设置环境变量的值。
如图所示:先打印了一开始的初始环境变量,接着重新设置环境变量,并打印输出。
###environvar.c
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221357175-1682965684.png)
- 将外部变量environ的内容打印出来,也就是把系统相关宏值,打印出来。
###testtty.c
程序代码
include <unistd.h>
int main()
{
char *buf = "abcde\n";
write(0, buf, 6);
}
运行结果
- 将缓冲区中的内容打印出来。
###sigactdemo.c
程序代码
include <stdio.h>
include <unistd.h>
include <signal.h>
define INPUTLEN 100
void inthandler();
int main()
{
struct sigaction newhandler;
sigset_t blocked;
char x[INPUTLEN];
newhandler.sa_handler = inthandler;
newhandler.sa_flags = SA_RESTART|SA_NODEFER
|SA_RESETHAND;
sigemptyset(&blocked);
sigaddset(&blocked, SIGQUIT);
newhandler.sa_mask = blocked;
if (sigaction(SIGINT, &newhandler, NULL) == -1)
perror("sigaction");
else
while (1) {
fgets(x, INPUTLEN, stdin);
printf("input: %s", x);
}
return 0;
}
void inthandler(int s)
{
printf("Called with signal %d\n", s);
sleep(s * 4);
printf("done handling signal %d\n", s);
}
运行结果
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127221506643-1267405852.png)
- 参数结构sigaction定义如下
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
flag
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
函数sigaction
int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);
sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。
#补充第五周的实验楼gdb实践
[GDB调试汇编堆栈过程分析](http://www.cnblogs.com/nostalgia-/p/6114629.html)
#心得体会
- 26年前的今天,中国哲学家冯友兰先生逝世。
- 冯友兰先生曾经点明:精其选,解其言,知其意,明其理。精其选就是读众口皆碑的经典著作,解其言就是解决语言文字的障碍,知其意就是常悟弦外之音,四是体会书中所言与客观事实的差距。
- 有道是,书不尽言,言不尽意。即便是经典著作也有与客观真理不相符的地方,因此读书仅得其意还不行,还需明其理,才不至于为前人所误。
#本周代码托管
- 托管截图及代码行数统计
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127210517784-852982213.png)
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127210532425-1240334466.png)
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127210547018-542172775.png)
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127210555331-1986098363.png)
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127210603534-942406628.png)
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127210612612-164709378.png)
![](http://images2015.cnblogs.com/blog/885886/201611/885886-20161127210620253-934799626.png)
- [代码链接](https://git.oschina.net/nostalgia_/Information-Security-System/tree/master/src/Week11?dir=1&filepath=src%2FWeek11&oid=fe68bc1fa574c600436cbb26bb27053c52ddf935&sha=1fa7596262e67dd97205bfa84e379c1fcacaee75)
#学习进度条
| | 代码行数(新增/累积)| 博客量(新增/累积)|学习时间(新增/累积)|重要成长|
| -------- | :----------------:|:----------------:|:---------------: |:-----:|
| 目标 | 5000行 | 30篇 | 400小时 | |
| 第一周 | 120/200 | 1/2 | 16/16 | 学习Linux核心命令 |
| 第二周 | 100/200 | 1/3 | 30/46 | 学习vim,gcc以及gdb的基本操作 |
| 第三周 | 30/230 | 1/4 | 15/61 | 对信息的表示和处理有更深入的理解 |
| 第四周 | 30/260 | 1/5 | 22/83 | 双系统的探索 |
| 第五周 | 130/390 | 1/6 | 25/108 | 汇编的深入学习 |
| 第六周 | 60/450 | 1/7 | 25/133 | 熟悉了Y86模拟器 |
| 第七周 | 60/510 | 2/9 | 20/153 | 掌握局部性原理 |
| 第八周 | 0/510 | 2/11 | 16/169 | 期中总结 |
| 第九周 | 132/642 | 1/12 | 21/190 | 深入理解系统级I/O |
| 第十周 | 132/642 | 1/13 | 20/210 | 对常用指令代码进行深入理解 |
| 第十一周 | 1003/1645 | 1/14 | 26/236 | 对系统调用有了更深的认识 |
## 参考资料
- [《深入理解计算机系统V2》学习指导](http://www.cnblogs.com/rocedu/p/5826467.html)
- [《信息安全系统设计基础》 课程教学](http://www.cnblogs.com/rocedu/p/5826175.html)
- [别出心裁的Linux命令学习法](http://www.cnblogs.com/rocedu/p/5826175.html)
- [现代软件工程讲义 0 课程概述](http://www.cnblogs.com/xinz/archive/2011/05/16/2048044.html)
- [现代软件工程 习而学的软件工程教育](http://www.cnblogs.com/xinz/archive/2012/01/08/2316717.html)
- [现代软件工程讲义 1 软件工程概论](http://www.cnblogs.com/xinz/archive/2011/05/22/2053838.html)
- [代码驱动的程序设计学习](http://www.cnblogs.com/nostalgia-/p/6013086.html)![]