20145221 《信息安全系统设计基础》第11周学习总结
异常控制流
异常
- 异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是有操作系统实现的。
- 异常:控制流中的突变,用来响应处理器状态中的某些变化。
- 在处理器中,状态被编码为不同的位和信号。状态变化成为事件。
- 异常表:当处理器监测到有时间发生时,通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。
- 当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况的一种:
- 处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令。
- 处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
异常处理
- 系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。
- 处理器的设计者:被除零、缺页、存储器访问违例、断点以及算数溢出。
- 操作系统内核的设计者分配的:系统调用和来自意外不I/O设备的信号。
- 异常号:到异常表中的索引
- 异常表基址寄存器:异常表的起始地址存放的位置。
- 异常与过程调用的异同:
- 过程调用时,在跳转到处理器之前,处理器将返回地址压入栈中。然而,根据异常的类型,返回地址要么是当前指令,要么是下一条指令。
- 处理器把一些额外的处理器状态压入栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。
- 如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核栈中,而不是压到用户栈中。
- 异常处理程序运行在内核模式下,意味着它们对所有的系统资源都有完全的访问权限。
异常的类别
- 中断:异步发生,是来自处理器外部的I/O设备的信号的结果。
- 硬件异常中断处理程序通常称为中断处理程序。
- 异步异常是有处理器外部的I/O设备中的时间产生的,同步异常是执行一条指令的直接产物。
- 陷阱、故障、终止时同步发生的,是执行当前指令的结果,我们把这类指令叫做故障指令。
- 陷阱和系统调用:
- 陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
- 普通的函数运行在用户模式中,用户模式限制了函数可以执行的指令的类型,而且它们只能访问与调用函数相同的栈。系统调用运行在内核模式中,内核模式允许系统调用执行指令,并访问定义在内核中的栈。
- 陷阱是有意的异常,最重要的用途是在用户程序和内核之间提供一个向过程一样的接口,叫做系统调用。
- 为了允许内核服务的受控访问,使用“syscall n”指令,跳转到一个异常处理程序的陷阱,处理程序对参数解码并调用适当的内核程序。
- 故障:是由错误情况引起的。
- 例如:abort例程会终止引起故障的应用程序。 根据故障是否能够被修复,故障处理程序要么重新执行引起故障的指令,要么终止。例如:缺页故障。
- 终止:是不可恢复的致命错误造成的结果,通常是一些硬件错误。
- 终止处理程序从不将控制返回给应用程序。
- 终止处理程序将控制直接返回给abort例程,直接终止该应用程序。
Linux/A32系统中的异常
- 031号:由intel架构师定义的异常。32255号:操作系统定义的中断和陷阱。
- linux/IA32故障和终止:
- 除法错误(异常0,linux中称为浮点异常)
- 一般保护故障(异常13,linux中称为段故障)
- 缺页(异常14)
- 机器检查(异常18)
- linux/IA32系统调用
进程
- 进程,就是一个执行中的程序的实例,系统中的每个程序都是定义在运行在某个进程的上下文中的。异常是允许操作系统提供进程的概念所需要的基的本构造块。
- 进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,独占地使用处理器;
- 一个私有的地址空间,独占地使用存储器系统。
逻辑控制流
- PC的值唯一地对应于包含在程序可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流。
- 进程轮流使用处理器,每个进程执行流的一部分,然后被抢占(暂时挂起)。
并发流
- 并发流:一个逻辑流的执行在时间上与另一个流重叠。
- 并发:多个流并发地执行的一般现象。
- 多任务:一个进程和其他进程轮流运行的概念。
- 时间片:一个进程执行它的控制流的一部分的每一时间段。
- 多任务也叫时间分片。
- 并行流:如果两个流并发的运行在不同的处理器核或者计算机上
私有地址空间
- 一个进程为每个程序提供他自己的私有地址空间,一般而言,和这个空间中某个地址相关联的存储器字节不能被其他程序读或写。
- 地址空间底部是保留给用户程序的,顶部保留给保留给内核,用来存放内核在代表进程执行时的指令。
用户模式和内核模式
- 模式位:用某个控制寄存器中的一个位模式,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
- 当设置了位模式,进程就运行在内核模式中,一个运行在内核模式中的进程可以中兴指令集中的任何指令,而且可以访问系统中任何存储器位置。
- 没有设置位模式时,进程就运行在用户模式中,不允许执行特权指令,例如停止处理器、改变位模式,或者发起一个I/O操作。
- 用户程序必须通过系统调用接口间接的当问内核代码和数据。
- 进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、或者陷入系统调用这样的异常。
上下文切换
- 上下文就是内核重新启动一个被抢占的进程所需的状态。
- 调度:内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。有内核中称为调度器的代码处理的。
- 上下文切换机制:
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
- 引起上下文切换的情况
- 当内核代表用户执行系统调用时
- 中断时
系统调用错误处理
- Unix系统级函数遇到错误时,会典型地返回-1,并设置全局变量errno来表示出错内容。
- 通过使用错误处理包装函数,可以进一步简化代码。
进程控制
获取进程ID
- 每个进程有一个唯一的非零正数进程ID(PID)。
pid_t getpid(void); /*返回调用进程的PID*/
pid_t getppid(void); /*返回它的父进程的PID*/
创建和终止进程
- 进程总处于三种状态
- 运行:进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
- 停止:程序的执行被挂起,,且不会被调度。
- 终止:进程用永远停止了。终止原因:(1)收到一个信号,默认行为是终止进程;(2)从主进程返回(3)调用exit函数。
- 父进程通过调用fork函数创建一个新的运行的子进程。
- 子进程和父进程的异同:
- 异:有不同的PID
- 同:用户级虚拟地址空间,包括:文本、数据和bss段、堆以及用户栈。任何打开文件描述符,子进程可以读写父进程中打开的任何文件。
fork
函数: 因为父进程的PID总是非零的,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。fork
函数的特点:- 调用一次,返回两次
- 并发执行
- 相同的但是独立的地址空间
- 共享文件
回收子进程
- 当一个进程由于某种原因终止,内核不会将他马上清除,而是将进程保持在已终止的状态中,直到被他的父进程回收。
- 当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。
一个终止了但还未被回收的进程称为僵死进程。 - 一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。默认地,当option=0时,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
//返回:若成功,返回子进程的PID;若WNOHANG,返回0;若其他错误,返回-1。
- 判定等待集合的成员,通过参数
pid
来确定的:pid>0
:等待集合是一个单独的子进程,进程ID等于pid。pid=-1
:等待结合就是由父进程所有的子进程组成的。
- 修改默认行为,通过
options
设置:WNOHANG
:默认行为是挂起调用进程。WUNTRACED
:默认行为是只返回已终止的子进程。WNOHANG|WUNTRACED
:立即返回,如果等待集合中没有任何子进程被停止或者已终止,那么返回值为0,或者返回值等于那个被停止或者已经终止的子进程的PID。
- 检查已回收子进程的退出状态,
wait.h
头文件定义了解释status参数
的几个宏:WIFEXITED
:如果子进程通过调用exit或者一个返回正常终止,就返回真;WEXITSTATUS
:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回真时,才会定义这个状态。
- 错误条件
- 如果调用进程没有子进程,那么waitpid返回-1,并设置errno为ECHILD
- 如果函数被一个信号中断,那么返回-1,并设置errno为EINTR。
- wait函数:调用
wait(&status)
等价于调用waitpid(-1.&status,0)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
//返回:若成功,返回子进程的PID;若错误,返回-1。
让进程休眠
- sleep函数:将进程挂起一段指定的时间
#include <unistd.h>
unsigned int sleep(unsigned int secs);
//返回:还要休眠的秒数
- pause函数:让调用函数休眠,直到该进程收到一个信号
#include <unistd.h>
int pause(void);
//返回:总是-1
加载并运行程序
- execve函数:在当前进程的上下文中加载并运行一个新程序。参数相关说明:
- filename:可执行目标文件
- argv:带参数列表
- envp:环境变量列表
- 特点:execve调用一次从不返回
#include <unistd.h>
int execve(const char *filename,const char *argv[],const char *envp[]);
//返回:若成功,则不返回,若错误,返回-1
-
注意:execve函数在当前进程的上下文中加载并运行一个新的进程。它会覆盖当前进程的地址空间,并没有创建一个新的进程,新的进程仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
-
getenv函数:在环境数组中搜素字符串“name =VALUE”,若找到了,就返回一个指向value的指针,否则它就返回NULL。
#include <stdlib.h>
char *getenv(const char *name);
//返回:存在,返回指向name的指针,若无匹配的,为NULL
利用fork和execve运行程序
- 外壳是一个交互型的应用级程序,它代表用户运行其他程序。
- 外壳执行一系统的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解释命令行,并代表用户运行程序。
信号
- 信号:一条小消息,通知进程系统中发生了一个某种类型的事件。
信号术语
- 发送信号:内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。
- 接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。通过信号处理程序捕获信号。
发送信号
- 进程组
- 每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。getpgrp函数返回当前进程的进程组ID:默认地,一个子进程和它的父进程同属于一个进程组。
- 用/bin/kill/程序发送信号 一个为负的PID会导致信号被发送到进程组PID中的每个进程。
- 从键盘发送信号
- 作业:表示对一个命令行求值而创建的进程。外壳为每个作业创建一个独立的进程组。
- 用kill函数发送信号
- 进程通过调用kill函数发送信号给其他的进程。父进程用kill函数发送SIGKILL信号给它的子进程。
- 用alarm函数发送信号
- 在任何情况下,对alarm的调用都将取消任何待处理的闹钟,并且返回任何待处理的闹钟在被发送前还剩下的秒数。
接收信号
- 内核从一个异常处理的程序返回,准备将控制传递给p时,会检查p未被阻塞的待处理信号的集合,若集合为空,则转到p逻辑控制流中的下一条指令;若集合不为空,则内核选择集合中的某个信号k,并且强制p接受信号k,触发进程的某种行为,完成后转到p逻辑控制流中的下一条指令。
- k的默认行为有:
- 进程终止。
- 进程终止并转储存储器。
- 进程停止直到被SIGCONT信号重启。
- 进程忽略该信号。
信号处理问题
- 待处理信号被阻塞:
- 待处理信号不会排队等待;
- 系统调用可以被中断:
- 像read、wait、accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。
- 注意:不可以用信号来对其他进程中发生的事件计数。
可移植的信号处理
- 信号处理语义的差异,是UNIX信号处理的一个缺陷。
显式地阻塞和取消阻塞信号
-
sigprocmask函数改变当前已阻塞信号的信号
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
-
sigprocmask函数改变当前已阻塞信号的集合,具体行为依赖how的值:
- SIG_BLOCK:添加set中的信号到blocked中。
- SIG_UNBLOCK:从blocked中删除set中的信号。
- SIG_SETMASK:blocked=set。
同步流以避免讨厌的并发错误
- 基本的问题是以某种方式同步并发流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
非本地跳转
- c语言中,用户级的异常控制流形式,通过setjmp和longjmp函数提供。
- setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0.
- 调用环境:程序计数器,栈指针,通用目的寄存器
- longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval
- 注:setjmp函数只被调用一次,但返回多次;longjmp函数被调用一次,但从不返回。
操作进程的工具
- STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。
- PS:列出当前系统中的进程(包括僵死的进程)。
- TOP:打印出关于当前进程资源使用的信息。
- PMAP:显示进程的存储器映射。
- /proc:一个虚拟文件系统,以ASCII格式输出大量内核数据结构内容。
教材学习中的问题和解决过程
testbuf1.c
-
运行截图:
-
说明:
fflush(stdout);
的作用是立刻将缓冲区里的内容强制输出- 其实平时,加不加这个无所谓,运行到这块都能输出,但是在用到
fork()
时,输出结果就不一样了,不会管后面子进程是否有输出没有,都会直接把父进程的输出内容输出。 - 还例如:
fflush(stdout);
,前面如果存在printf("..") //不含" "
,也会对输出顺序产生很大改变,详见下例。
testbuf3.c
-
运行截图:
-
说明:
-
printf(或fprintf)输出其实不是立马输出的,都是先有一个缓冲区,存在缓冲区里满足相应条件再输出。在这里,2句fprintf代码将输出内容分别指定到strerr和stdout,而strerr是无缓冲( unbuffered)的可以立马输出到终端,stdout是有缓冲(line-buffered)的,遇到换行或者缓冲区满再做flush;所以此处优先在屏幕打印stderr的内容。
-
The stream stderr is unbuffered. The stream stdout is line-buffered when it points to a terminal. Partial lines will not appear until fflush(3) or exit(3) is called, or a newline is printed. This can produce unexpected results, especially with debugging output. The buffering mode of the standard streams (or any other stream) can be changed using the setbuf(3) or setvbuf(3) call.
-
可以试试在两个fprintf之间加上代码:
fflush(stdout)
,这样可以强制stdout缓冲区里的内容立马输出。 -
代码中
stdout
:标准输出设备 -
代码中
stderr
:标准错误输出设备,两者默认向屏幕输出。 -
但如果用转向标准输出到磁盘文件,则可看出两者区别。stdout输出到磁盘文件,stderr在屏幕。
-
例如,重定向:
./testbuf3 > testbuf3.txt
,结果如下图(stdout输出到磁盘文件,stderr在屏幕):
-
forkdemo.c
-
老师给的前3个
forkdemo.c
都比较好理解(再弄清楚fork函数4个特点后,前文有提及) -
在此我为了能更清楚地了解一下fork调用父子进程的过程,编写了一个简单的代码:
#include <stdio.h> #include <unistd.h> int main() { fork(); fork(); fork(); printf("my pid is %d ", getpid()); return 0; }
-
运行截图:
-
说明:
-
如上图所示,假设父进程的
pid=i
,大多数时候执行该代码的结果pid顺序为:i i+3 i+2 i+1 i+4 i+6 i+5 i+7
;极少数的时候,出现第二个框里面的pid次序:i i+3 i+2 i+1 i+6 i+4 i+5 i+7
。 -
对此我思考了一下,画了一张图(在我的理解里,我认为这就是父子进程的pid,图中
f:父进程 s:子进程
) -
如果我标的
pid
正确的话,那么可以看出来打印时优先打印图中第3个fork()
后产生的f进程(父进程),最后打印其余的s进程(子进程) -
但是我不能理解为什么是这个顺序,最后耐心看了课本后,其中有一段话可能表示我做这些都是徒劳没意义的
-
当我们在系统上运行这个程序时,父进程优先完成它的printf语句,然后是子进程。然而,在另一个系统上正好相反。一般而言,作为程序员,我们绝不能对不同进程中指令的交替执行做任何假设。 ——《CSAPP》 P493
-
forkgdb.c
-
运行截图:
-
说明:
- 过程:父进程打印是先打印两句,然后休眠一秒,此时子进程打印一句,然后休眠一秒,接着父进程打印一句后进入第二个循环接着打印两句,休眠,交给子进程……
- 这与我在运行代码前的假设不同,我开始想当然的以为sleep那子进程会等待父进程,或者父等子,其实谁也没等谁,当一个进程在sleep时,另一个进程就会抢占执行;
- 这很生动的说明了这两个进程是并发的,所以可以看到在一个进程休眠的那一秒,另一个进程在执行,并且进程之间相互独立互不干扰,也就是前文提到的相同的但是独立的地址空间这个特点。
本周代码托管
-
代码统计:
-
代码托管:
其他(感悟、思考等,可选)
- 这周代码较多,很多函数也很陌生,课本第八章节的内容也较多,所以开始学起来有点无从下手,最后看了看2016-2017-1 《信息安全系统设计基础》教学进程这篇博客,下周的任务是继续巩固代码,多实践;所以,我将一部分重心放在了知识点的整理上,另外,对教学进程中本周基本重要掌握的函数进行了编译运行,体会过程。
- 感触最深的还是代码编译,老师之前就强调过拿到代码不要着急去
gcc
,那样谁都会。而是对于结构简单的代码,在编译执行前好好思考代码应该输出的结果,最后与实际运行结果比对得出相应的结论;对于结构层次复杂的代码,要在输出结果的辅助下,尽可能地去理解代码的功能作用。只有这样,当你编译调试完一个代码后才会有很大的成就感,才会有收获。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 20/20 | 学习了Linux常用命令 |
第二周 | 79/279 | 1/3 | 30/50 | 了解vim,gcc,gdb基本操作 |
第三周 | 182/461 | 1/4 | 25/75 | 更深层次了解信息处理 |
第四周 | 36/497 | 2/6 | 3/78 | 第二章知识简单的运用 |
第五周 | 194/691 | 1/7 | 28/106 | 汇编知识与了解逆向 |
第六周 | 520/1211 | 1/8 | 27/133 | Y86处理器,了解ISA抽象 |
第七周 | 85/1296 | 1/9 | 21/153 | 理解了局部性原理 |
第八周 | 0/1296 | 2/11 | 20/173 | 期中总结 |
第九周 | 234/1530 | 2/13 | 30/203 | 了解Linux是怎样操作文件 |
第十周 | 453/1983 | 2/15 | 20/223 | 系统调用深入实践 |
第十一周 | 1097/3080 | 3/18 | 26/249 | 理解进程和并发,了解异常及其种类 |