20145231 《信息安全系统设计基础》第11周学习总结
20145231《信息安全系统设计基础》第11周学习总结
教材学习内容总结
异常
异常是异常控制流的一种形式,由硬件和操作系统实现。简单来说,就是控制流中的突变。
出现异常的处理方式:
1.处理器检测到有异常发生
2.通过异常表,进行间接过程调用,到达异常处理程序
3.完成处理后:①返回给当前指令②返回给下一条指令③终止
1、异常处理
异常号:系统为每种类型的异常分配的唯一的非负整数。
异常表:系统启动时操作系统就会初始化一张条转变,使得条目k包含异常k的处理程序的地址。
异常号是到异常表中的索引,异常表的起始地址放在异常表基址寄存器。
异常类似于过程调用,区别在:
1.处理器压入栈的返回地址,是当前指令地址或者下一条指令地址。
2.处理器也把一些额外的处理器状态压到栈里
3.如果控制一个用户程序到内核,所有项目都压到内核栈里。
4.异常处理程序运行在内核模式下,对所有的系统资源都有完全的访问权限。
2、异常的类别
故障指令:执行当前指令导致异常(陷阱、故障、终止)
中断处理程序:硬件中断的异常处理程序(中断)
异常的类别如下图。异步异常时有处理器外部的I/O设备中的事件产生的,同步异常时执行一条指令的直接产物
陷阱是有意的异常,是执行一条指令的结果,最重要的用途——系统调用
故障是由错误状况引起,可能能够被故障处理程序修正。结果要么重新执行指令(就是返回当前指令地址),要么终止。典型示例:缺页异常
3、Linux/IA32系统中的异常
IA32系统中的异常列表如下
每一个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量
在IA32中,系统调用通过一条陷阱指令提供:
int n;//n为异常号
所有的到Linux系统调用的参数都是通过寄存器传递的。惯例如下:
%eax:包含系统调用号
%ebx,%ecx,%edx,%esi,%edi,%ebp:包含最多六个任意参数
%esp:栈指针,不能使用
进程
进程的经典定义:一个执行中的程序的实例。
系统中的每个程序都是运行在某个进程的上下文中的。
上下文:由程序正确运行所需的状态组成的。
进程提供给应用程序的关键抽象:
一个独立的逻辑控制流:独占的使用处理器
一个私有的地址空间:独占的使用存储器系统
1、逻辑控制流
一系列的程序计数器PC的值,分别唯一的对应于包含子啊程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令,这个PC值的序列就叫做逻辑控制流。
进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。但是进程可以向每个程序提供一种假象,好像它在独占的使用处理器。
逻辑流示例:异常处理程序、进程、信号处理程序、线程、Java进程
2、并发流
一个逻辑流的执行在时间上与另一个流重叠。(与是否在同一处理器无关)
两个流并发的运行在不同的处理机核或者计算机上。
并行流并行的运行,并行的执行。
3、私有地址空间
进程为程序提供的假象,好像它独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读写的。
4、用户模式和内核模式
用户模式和内核模式的区别就在于用户的权限上,权限指的是对系统资源使用的权限。
具体的区别是有无模式位,有的话就是内核模式,可以执行指令集中的所有指令,访问系统中任何存储器位置;没有就是用户模式。
进程从用户模式变为内核模式的唯一方法是通过异常——中断,故障,或者陷入系统调用。
Linux的聪明机制——/proc文件系统,将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。
5、上下文切换
操作系统内核使用上下文切换这种较高层形式的异常控制流来实现多任务。上下文切换机制建立在较底层异常机制之上。
上下文:内核重新启动一个被抢占的进程所需的状态。由一些对象的值组成:
•通用目的寄存器
•浮点寄存器
•程序计数器
•用户栈
•状态寄存器
•内核栈
•内核数据结构:页表、进程表、文件表
上下文切换机制:
1.保存当前进程的上下文
2.恢复某个先前被抢占的进程被保存的上下文
3.将控制传递给这个新恢复的进程。
可能发生上下文切换的原因:
•内核代表用户执行系统调用时
•中断
系统调用错误处理
系统会使用错误处理包装函数,系统级函数是小写,他们的包装函数名大写,包装函数调用基本函数,有任何问题就终止,如果没有问题和基本函数是一样的。
进程控制
1、获取进程ID
每个进程都有一个唯一的正数进程ID(PID)。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void)
; 返回调用进程的PID
pid_t getppid(void)
; 返回父进程的PID(创建调用进程的进程)
2、创建和终止进程
进程总是处于下面三种状态之一:运行;停止:被挂起且不会被调度;终止:永远停止
终止的原因:
1.收到信号,默认行为为终止进程
2.从主程序返回
3.调用exit函数
父进程通过调用fork函数来创建一个新的运行子进程。fork函数定义如下:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork函数只被调用一次,但是会返回两次:父进程返回子进程的PID,子进程返回0.如果失败返回-1
调用fork函数n次,产生2的n次方个进程。
终止进程用exit函数:
#include <stdlib.h>
void exit(int status);
exit函数以status退出状态来终止进程
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.
判断等待集合的成员——pid:
•pid>0:等待集合是一个单独子进程,进程ID等于pid
•pid=-1:等待集合是由父进程所有的子进程组成
•其他
修改默认行为——options:
检查已回收子进程的退出状态——status,在wait.h头文件中定义了解释status参数的几个宏:
•WIFEXITED:如果子进程通过调用exit或一个返回正常终止,就返回真
•WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态
•WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么返回真
•WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED返回为真时才定义这个状态
•WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么返回真
•WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时才定义这个状态
错误条件:如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。
如果waitpid被一个信号中断,那么他返回-1,并且设置errno为EINTR。
wait函数是waitpid函数的简单版本,wait(&status)等价于waitpid(-1,&status,0).成功返回子进程pid,出错返回-1:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
4、让进程休眠
sleep函数使一个进程挂起一段指定的时间。定义如下:
#include <unistd.h>
signed int sleep(unsigned int secs);
返回值是剩下还要休眠的秒数,如果到了返回0.
pause函数让调用函数休眠,直到该进程收到一个信号:
#include <unistd.h>
int pause(void);
5、加载并运行程序——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,继承文件描述符。
信号
1、信号术语
传递一个信号到目的进程的两个步骤:发送信号和接收信号。
发送信号的原因:
1.内核检测到一个系统事件
2.一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。
一个进程可以发送信号给它自己。
接收信号:
1.忽略
2.终止
3.执行信号处理程序,捕获信号
待处理信号:
•只发出没有被接收的信号
•任何时刻,一种类型至多只会有一个待处理信号,多的会被直接丢弃
•一个进程可以选择性的阻塞接受某种信号,被阻塞仍可以被发送,但是不会被接收
•一个待处理信号最多只能被接收一次。
•pending:待处理信号集合
•blocked:被阻塞信号集合。
2、发送信号——基于进程组
进程组:
•每个进程都只属于一个进程组。
•进程组ID:正整数
•一个子进程和他的父进程属于同一进程组。
•查看进程组id:getpgrp
•修改进程组:setpgid
/bin/kill
程序可以向另外的进程发送任意的信号,格式是:
/bin/kill -n m
n是信号,m是进程或进程组
当n>0时,发送信号n到进程m
当n<0时,使信号|n|发送到进程组m中的所有进程。
进程通过调用kill函数发送信号给其他进程。
进程可以通过调用alarm函数向它自己发送SIGALRM信号
#include <unistd.h>
unsigned int alarm(unsigned int secs);
返回前一次闹钟剩余的秒数,若没有返回0.
非本地跳转
c语言中,用户级的异常控制流形式,通过setjmp和longjmp函数提供。
setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0.
longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。
然后setjmp返回,并带有非零的返回值retval
注:setjmp函数只被调用一次,但返回多次;longjmp函数被调用一次,但从不返回。
操作进程的工具
STRACE:打印一个正在运行的程序和他的子程序调用的每个系统调用的痕迹
PS:列出当前系统中的进程,包括僵死进程
TOP:打印出关于当前进程资源使用的信息
PMAP:显示进程的存储器映射
实践学习总结
数组指针、指针数组、函数指针、指针函数的区别
1.指针数组是数组,数组里的元素是指针
int *daytab[13]
2.数组指针是指针,指向一个类型和元素个数都固定的数组
int (*daytab1)[13]
3.指针函数是函数,返回值类型是指针
int *comp()
4.函数指针是指针,指向函数的指针,函数名就是函数指针
int (*comp1)()
关于exec.1
exec1.c代码运行如下
exec1.c中execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中
exevp函数调用成功没有返回,所以没有打印出“* * * ls is done. bye”这句话
想要更深入的理解这段代码,于是将ls -l变换成man -k,修改代码
重新编译运行结果如下
结果同样也是执行了man -k语句,还是没有返回“* * * man is done. bye”
关于exec2.c
exec2.c代码运行如下
exec2与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],不过由定义可得这两个等价,所以运行结果是相同的
若将exevp函数传入的arglist[0]改为arglist[1],此时exevp函数没有调用成功,于是打印出“* * * ls is done. bye”这句话
关于exec3.c
exec3.c代码运行如下
函数中execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……最后一个参数必须用空指针(NULL)作结束
以下四个函数中任意一个的运行结果都与其他三个完全一致。
execv()``execvp()``execvpe()``execlp()
关于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与之后一句
运行如下
关于forkgdb.c
父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰
运行如下
关于waitdemo1.c
waitdemo1.c的功能是如果有子进程,则终止子进程,成功返回子进程pid。运行如下
关于waitdemo2.c
waitdemo2.c比起1来就是多了一个子进程的状态区分,把状态拆分成三块,exit,sig和core
运行如下
关于testbuf
testbuf1.c代码运行如下
testbuf2.c代码运行如下
testbuf1.c和testbuf2.c代码运行结果一致,因为fflush(stdout)的效果和换行符\n是一样的
testbuf3.c将内容格式化输出到标准错误、输出流中
testpid.c代码输出当前进程pid和当前进程的父进程的pid
testsystem.c代码中system()——执行shell命令,也就是向dos发送一条指令。这里是后面可以跟两个参数,然后向dos发送这两个命令,分别执行
如下图,输入ls和dir两个指令后,可以看到分别执行了
关于psh1.c
psh1.c代码的效果是输入要执行的指令,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令
运行结果如下
关于psh2.c
psh2.c比起1来,多了循环判断,不退出的话就会一直要你输入指令,并且对于子程序存在的状态条件
运行如下
关于signal
sigdemo1.c程序连续输出五个hello,每两个之间的间隔时间为2秒,且在此期间输入的Ctrl+C都被处理成打印OUCH
sigdemo2.c一直输出haha,按Ctrl+C不能停止
sigactdemo2.c休息seconds秒后返回;或者被信号中断且信号处理函数返回后sleep()返回0。所以如果不计较返回值的话,pause()的功能相当于无限期的sleep()
sigdemo2.c中:
SIG_DFL,SIG_IGN 分别表示无返回值的函数指针,指针值分别是0和1,这两个指针值逻辑上讲是实际程序中不可能出现的函数地址值。
SIG_DFL:默认信号处理程序
SIG_IGN:忽略信号的处理程序
sigdemo3.c根据代码,在read函数不发生错误的情况下输入什么,就输出什么,输入的Ctrl+C也无法终止程序,只有输入quit的时候才会退出
sigactdemo.c中sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号
int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
学习过程中的问题和解决过程
教材学习中遇到的问题
对于教材中信号方面的代码不太理解。
实践中遇到的问题
编译forkdemo1.c的时候出错,根据提示修改源代码,加入#include <stdio.h>
重新编译即可
代码托管截图
代码统计
代码链接:(https://git.oschina.net/xzhkuma1128/CSAPP2E)
心得体会
本周内容比较多,不仅是课本知识更要花时间实践,对于代码的理解还有些问题,看了书对于信号这部分真的不太理解,对于fork()函数因为老师上课时有讲解,所以再看书就觉得好理解多了,希望老师能再讲讲信号这部分知识。
学习进度条
博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|
目标 | 30篇 | 400小时 | |
第一周 | 1/1 | 20/20 | 学习了Linux核心命令 |
第二周 | 1/2 | 21/41 | 学习了vim、gcc、gdb指令 |
第三周 | 1/3 | 20/61 | 学习了信息的表示和处理,了解了二进制在计算机系统中的重要性 |
第五周 | 1/4 | 20/81 | 学习了机器级程序,读懂汇编代码 |
第六周 | 1/5 | 19/100 | 了解了处理器对于指令的处理过程 |
第七周 | 1/6 | 18/118 | 了解了存储器层次结构及存储技术 |
第八周 | 2/8 | 15/133 | 对前几周内容进行复习 |
第十周 | 3/12 | 10/148 | 学习Linux重要命令 |
第十一周 | 2/14 | 15/163 | 学习异常控制相关知识 |