20155239 《信息安全系统设计基础》第14周学习总结
20155239 《信息安全系统设计基础》第14周学习总结
学习目标
找出全书你认为学得最差的一章,深入重新学习一下,要求(期末占5分):主要学习第八章
总结新的收获
给你的结对学习搭档讲解或请教,并获取反馈
参考上面的学习总结模板,把学习过程通过博客(随笔)发表,博客标题“学号 《信息安全系统设计基础》第十四周学习总结”,博客(随笔)要通过作业提交,截至时间本周日 23:59。
异常
异常是异常控制流的一种形式,由硬件和操作系统实现。简单来说,就是控制流中的突变。
出现异常的处理方式:
1.处理器检测到有异常发生
2.通过异常表,进行间接过程调用,到达异常处理程序
3.完成处理后:
(1)返回给当前指令
(2)返回给下一条指令
(3)终止
异常处理
-
系统中可能的每种异常都被分配了唯一一个非负整数的异常号,异常表中的条目k中包含异常k的处理程序地址。异常表的起始地址存放在一个叫做异常表基址寄存器的特殊寄存器中。
-
异常类和过程调用的不同之处:
-
返回地址是当前地址或者下一条指令
-
处理器也会把额外的处理器状态压回栈中,在处理程序返回时,重新开始被中断的程序会需要这些状态。
-
如果控制从一个用户程序转移到内核,那么所有项目都会被压到内核栈中而不是用户栈。
-
异常处理程序运行在内核模式下,意味着他们对所有的系统资源拥有完全的访问权限。
异常类别
1.中断
- 中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。因此是异步的。硬件中断的异常处理程序通常称为中断处理程序。
- 其余异常类型都是同步发生的,是执行当前指令的结果。这一类指令称为故障指令。
2.陷阱
- 陷阱是有意的异常,最重要的用途是在用户程序和内核之间提供一个向过程一样的接口,叫做系统调用。
- 为了允许内核服务的受控访问,使用“syscall n”指令,跳转到一个异常处理程序的陷阱,处理程序对参数解码并调用适当的内核程序。
3.故障
- 故障由错误情况引起,可能能够被故障处理程序修正。故障发生时,处理器将控制转移给故障处理程序,若能修正,则将控制返回到引起故障的指令,重新执行;若不能修正,处理程序返回abort例程,终止引起故障的应用程序。
4.终止
- 终止是不可恢复的致命错误造成的结果,通常是硬件错误。终止处理程序将控制直接返回给abort例程,直接终止该应用程序。
Linux/A32系统中的异常
- Linux/A32故障和终止
- 除法错误(异常0):应用试图除以0,或者除法指令的结果对于目标操作数过大。
- 一般故障保护(异常13):通常因为一个程序引用一个未定义的虚拟存储区域,或者试图写一个只读文本段。
- 缺页(异常14):处理程序将磁盘上虚拟存储器相应页面映射到物理存储器的一个页面,然后重新开始执行这条指令。
- 机器检查(异常18):在导致故障的指令执行中检测到致命的硬件错误。
- Linux/A32系统调用
- 每个系统调用都对应着唯一的整数号,对应于一个到内核中跳转表的偏移量。
- IA32系统调用是通过一条称为 int n 的陷阱指令提供的。
- C程序通过syscall函数可以直接调用任何系统调用。
- 所有Linux系统调用都是通过通用寄存器而不是栈传递的,%eax包含系统调用号,%ebx、%ecx、%edx、esi%、%edi和%ebp包含最多6个参数。栈指针%esp不能使用,因为当进入内核模式时,内核会覆盖它。
进程
进程的经典定义:一个执行中的程序的实例。
系统中的每个程序都是运行在某个进程的上下文中的。
上下文:由程序正确运行所需的状态组成的。
1、逻辑控制流
-
一系列的程序计数器PC的值,分别唯一的对应于包含子啊程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令,这个PC值的序列就叫做逻辑控制流。
-
进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。但是进程可以向每个程序提供一种假象,好像它在独占的使用处理器。
-
逻辑流示例:异常处理程序、进程、信号处理程序、线程、Java进程
2、并发流
一个逻辑流的执行在时间上与另一个流重叠。(与是否在同一处理器无关)
两个流并发的运行在不同的处理机核或者计算机上。
并行流并行的运行,并行的执行。
3、私有地址空间
进程为程序提供的假象,好像它独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读写的。
4、用户模式和内核模式
用户模式和内核模式的区别就在于用户的权限上,权限指的是对系统资源使用的权限。
具体的区别是有无模式位,有的话就是内核模式,可以执行指令集中的所有指令,访问系统中任何存储器位置;没有就是用户模式。
进程从用户模式变为内核模式的唯一方法是通过异常——中断,故障,或者陷入系统调用。
进程控制
获取进程ID
每个进程有一个唯一的非零正数进程ID(PID)。
pid_t getpid(void); /*返回调用进程的PID*/
pid_t getppid(void); /*返回它的父进程的PID*/
创建和终止进程
进程总是处于以下三种状态之一:
- 运行:在CPU上执行,或者等待被执行且最终会被内核调度。
- 停止:进程的执行被挂起,且不会被调度。(与信号有关)
- 终止:进程永远停止。进程终止的原因:1)收到一个信号,默认行为是终止程序2)从主程序返回3)
回收子进程
- 进程终止后还要被父进程回收,否则处于僵死状态。
- 如果父进程没有来得及回收,内核会安排init进程来回收他们。init进程的PID为1.
一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。waitpid函数的定义如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
- 成功返回子进程PID,如果WNOHANG,返回0,其他错误返回-1.
1.判断等待集合的成员——pid
pid>0:等待集合是一个单独子进程,进程ID等于pid
pid=-1:等待集合是由父进程所有的子进程组成
其他。
2.修改默认行为——options
设置为常量WNOHANG和WUNTRACED的各种组合:
3.检查已回收子进程的退出状态——status
在wait.h头文件中定义了解释status参数的几个宏:
WIFEXITED:如果子进程通过调用exit或一个返回正常终止,就返回真
WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态
WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么返回真
WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED返回为真时才定义这个状态
WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么返回真
WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时才定义这个状态
4.错误条件
如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。
如果waitpid被一个信号中断,那么他返回-1,并且设置errno为EINTR。
让进程休眠
sleep函数使一个进程挂起一段指定的时间。定义如下:
#include <unistd.h>
signed int sleep(unsigned int secs);
返回值是剩下还要休眠的秒数,如果到了返回0.
pause函数让调用函数休眠,直到该进程收到一个信号:
#include <unistd.h>
int pause(void);
加载并运行程序——execve函数
execve函数调用一次,从不返回:
#include <unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);
成功不返回,失败返回-1.
getnev函数在环境数组中搜寻字符串"name=value",如果找到了就返回一个指向value的指针,否则返回null:
#include <stdlib.h>
char *getenv(const char *name);
若存在则为指向name的指针,无匹配是null
setenv和unsetenv函数:如果环境数组包含"name=oldvalue"的字符串,unsetenv会删除它,setenv会用newvalue代替oldvalue,只有在overwrite非零时成立。
如果name不存在,setenv会将"name=newvalue"写进数组。
#include <stdlib.h>
int setenv(const char *name, const char *newvalue, int overwrite);
若成功返回0,错误返回-1
void unsetenv(const char *name);
无返回值
fork函数和execve函数的区别
-
fork函数是创建新的子进程,是父进程的复制体,在新的子进程中运行相同的程序,父进程和子进程有相同的文件表,但是不同的PID
-
execve函数在当前进程的上下文中加载并运行一个新的程序,会覆盖当前进程的地址空间,但是没有创建一个新进程,有相同的PID,继承文件描述符。
非本地跳转
-
c语言中,用户级的异常控制流形式,通过setjmp和longjmp函数提供。
-
setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0.
-
longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval
注:setjmp函数只被调用一次,但返回多次;longjmp函数被调用一次,但从不返回。
操作进程的工具
-
STRACE:打印一个正在运行的程序和他的子程序调用的每个系统调用的痕迹
-
PS:列出当前系统中的进程,包括僵死进程
-
TOP:打印出关于当前进程资源使用的信息
-
PMAP:显示进程的存储器映射
代码调试
exec1
- exec1.c中execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件
- 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中
- exevp函数调用成功没有返回,所以没有打印出“* * * ls is done. bye”这句话
exec2
- exec2与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],不过由定义可得这两个等价,所以运行结果是相同的
env
- 代码中涉及到getenv函数和setenv函数
- getenv函数是获得环境变量值的函数,参数是环境变量名name,例如”HOME”或者”PATH”。如果环境变量存在,那么getenv函数会返回环境变量值,即value的首地址;如果环境变量不存在,那么getenv函数返回NULL
- setenv函数是修改或添加环境变量的函数
1.如果name在环境中不存在,那么很好办,在环境中添加这个新的变量就OK。
setenv函数必须在environment list中增加一个新的entry,然后动态申请存储空间来存储name=value,并且使entry指向该空间。
2.如果在环境中name已经存在,那么
(a)若overwrite非0,那么更新name的value(实质是更新环境表,指向新的value)
(b)若overwrite为0,则环境变量name不变,并且也不出错
- setenv函数不必在environment list中增加一个新的entry。当overwrite为0, 则不必改动entry的指向;当overwrite非0, 则直接使该entry指向name=value,当然该name=value也是存储在动态申请的内存里。
- environvar.c代码简单打印环境变量表,运行结果如下:
argv
头文件argv.h,下面的函数的功能是把命令行字符串转化为以NULL结尾的参数数组
开始的因为没有头文件,需要放在一个目录下
int makeargv(const char *s, const char *delimiters, char ***argvp);
- 其中s为命令行字符串,delimiters为分割符,argvp为指向参数数组的指针,如果转化成功则返回标记的个数,如果错误则返回-1,并设置errno
由于argtest.c中有如下代码:
if (argc != 2)
{
fprintf(stderr, "Usage: %s string\n", argv[0]);
return 1;
}
- 只有当输入命令的个数等于2时,才能显示命令正确的结果。
fifo
- FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中
- FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作
- FIFO往往都是多个写进程,一个读进程
- FIFO是一种文件类型,可以通过查看文件stat结构中的stmode成员的值来判断文件是否是FIFO文件。
- FIFO是用来在进程中使用文件来传输数据的,也具有管道特性,可以在数据读出的时候清除数据
forkgdb
- 过程:父进程打印是先打印两句,然后休眠一秒,此时子进程打印一句,然后休眠一秒,接着父进程打印一句后进入第二个循环接着打印两句,休眠,交给子进程……
- 当一个进程在sleep时,另一个进程就会抢占执行;
- 这很生动形象的说明了这两个进程是并发的,所以可以看到在一个进程休眠的那一秒,另一个进程在执行,并且进程之间相互独立互不干扰,也就是前文提到的相同的但是独立的地址空间这个特点。
testbuf1
- fflush(stdout);的作用是立刻将缓冲区里的内容强制输出
- 在用到fork()时,输出结果就不一样了,不会管后面子进程是否有输出没有,都会直接把父进程的输出内容输出。
testbuf3
- printf(或fprintf)输出其实不是立马输出的,都是先有一个缓冲区,存在缓冲区里满足相应条件再输出。在这里,2句fprintf代码将输出内容分别指定到strerr和stdout,而strerr是无缓冲( unbuffered)的可以立马输出到终端,stdout是有缓冲(line-buffered)的,遇到换行或者缓冲区满再做flush;所以此处优先在屏幕打印stderr的内容。
forkdemo1.c
- forkdemo1.c代码先是打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0
forkdemo2.c
这个代码调用两次fork,一共产生四个子进程,所以会打印四个aftre输出
forkdemo3.c
fork产生子进程,父进程返回子进程pid,不为0,所以输出父进程的那句话,子进程返回0,所以会输出子进程那句话
forkdemo4.c
先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,休眠十秒;子进程返回0,所以输出child与之后一句
consumer.c
producer.c
forkgdb.c
- 父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰![]