20135302魏静静——linux课程第七周实验及总结
linux课程第七周实验及总结
实验及学习总结
1. 编译链接的过程和ELF可执行文件格式(以hello为例)
GNU编译系统编译源码:
- 首先,运行C预处理器(cpp),将.c文件翻译成.i文件——gcc -E -o hello.cpp hello.c -m32
- 接着,运行C编译器(cc1),将.i文件翻译成ASCII汇编语言文件.s文件——gcc -S -o hello.s hello.cpp -m32
- 然后,运行汇编器(as),将.s文件翻译成可重定位目标文件.o文件——gcc -c hello.s -o hello.o -m32
- 最后,运行链接器(ld),将各.o文件组合起来,创建一个可执行目标文件——ld -o hello hello.o xxx.o
- 流程图:
ELF可执行文件格式
ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。
- 我们使用readelf -h命令查看hello的ELF文件头,可以看到:(除此外objump或readelf可以直接查看ELF文件各节点信息)
-
ELF文件头代码项含义:
最开头是16个字节的e_ident, 其中包含用以表示ELF文件的字符,以及其他一些与机器无关的信息。开头的4个字节值固定不变,为0x7f和ELF三个字符。e_type 它标识的是该文件的类型。e_machine 表明运行该程序需要的体系结构。e_version 表示文件的版本。e_entry 程序的入口地址。e_phoff 表示Program header table 在文件中的偏移量(以字节计数)。e_shoff 表示Section header table 在文件中的偏移量(以字节计数)。e_flags 对IA32而言,此项为0。e_ehsize 表示ELF header大小(以字节计数)。e_phentsize 表示Program header table中每一个条目的大小。e_phnum 表示Program header table中有多少个条目。e_shentsize 表示Section header table中的每一个条目的大小。e_shnum 表示Section header table中有多少个条目。e_shstrndx 包含节名称的字符串是第几个节(从零开始计数)
2. gdb调试execve系统调用及新的可执行程序起点
新的可执行程序起点
一般是地址空间为0x8048000或0x8048300;
- 与此相对的,再返回用户态时new_ip保存返回用户态的第一条指令的地址
execve系统调用
-
Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数
-
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
- execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。
-
库函数exec*都是execve的封装例程
execve和fork都是特殊一点的系统调用:一般的都是陷入到内核态再返回到用户态。
fork父进程和一般进程调度一样,子进程返回到一个特定的点ret_from_fork,子进程是从ret_from_fork开始执行然后返回到用户态;
execve特殊:执行到可执行程序--陷入内核--构造新的可执行文件--覆盖掉原可执行程序--返回到新的可执行程序,作为起点(也就是main函数) ,需要构造他的执行环境;
- 我们可以看一下execve系统调用在实验中的入口及返回值
- sys_execve()进行一些参数的检查复制之后,调用do_execve()。
do_execve()会首先检查被执行文件,当do_execve()读取了128个字节的文件头部之后,然后调用search_binary_handle()【retval = search_binary_handler(bprm,regs)】
去搜索和匹配合适的可执行文件装载处理过程。Linux中所有被支持的可执行文件格式都有相应的装载处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,
并且调用相应的装载处理过程。如ELF用load_elf_binary(),a.out用load_aout_binary(),脚本用load_script()。其中ELF装载过程的主要步骤是:
①检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(Segment)的数量。
②寻找动态链接的”.interp”段(该段保存可执行文件所需要的动态链接器的路径),设置动态链接器路径。
③根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。
④初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址(结束代码地址)。
⑤将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,
-
静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时:
execve系统调用将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,
对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_enEry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。
当ELF被load_elf_binary()装载完成后,函数返回至do_execve()在返回至sys_execve()。在load_elf_binary()中系统调用的返回地址已经被改成ELF程序的入口地址了。
所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。
3. exec*函数对应的系统调用处理过程
内核通过以下步骤实现该系统调用:
(1) exec系统调用要求以参数形式提供可执行文件名,并存储该参数以备将来使用。连同文件名一起,还要提供和存储其他参数,例如如果shell命令是"ls -l",那么ls作为文件名,-l作为选项,一同存储起来以备将来使用。
(2) 现在,内核解析文件路径名,从而得到该文件的索引节点号。然后,访问和读取该索引节点。内核知道对任何shell命令而言,它要先在/bin目录中搜索。
(3) 内核确定用户类别(是所有者、组还是其他)。然后从索引节点得到对应该可执行文件用户类别的执行(X)权限。内核检查该进程是否有权执行该文件。如果不可以,内核提示错误消息并退出。
(4) 如果一切正常,它访问可执行文件的头部。
(5) 现在,内核要将期望使用的程序(例如本例中的ls)的可执行文件加载到子进程的区域中。但"ls"所需的不同区域的大小与子进程已经存在的区域不同,因为它们是从父进程中复制过来的。因此,内核释放所有与子进程相关的区域。这是准备将可执行镜像中的新程序加载到子进程的区域中。在为仅仅存储在内存中的该系统调用存储参数后释放空间。进行存储是为了避免"ls"的可执行代码覆盖它们而导致它们丢失。根据实现方式的不同,在适当的地方进行存储。例如,如果"ls"是命令,"-l"是它的参数,那么就将"-l"存储在内核区。/bin目录中"ls"实用程序的二进制代码就是内核要加载到子进程内存空间中的内容。
(6) 然后,内核查询可执行文件(例如ls)镜像的头部之后分配所需大小的新区域。此时,建立区域表和页面映射表之间的链接。
(7) 内核将这些区域和子进程关联起来,也就是创建区域表和P区表之间的链接。
(8) 然后,内核将实际区域中的内容加载到分配的内存中。
(9) 内核使用可执行文件头部中的寄存器初始值创建保存寄存器上下文。
(10) 此时,子进程("ls"程序)已经运行。因此,内核根据子进程优先级,将其插到"准备就绪"进程列表的合适位置。最终,调度这个子进程。
(11) 在调度该子进程后,由前述(9)中介绍的保存寄存器上下文生成该进程的上下文。然后,PC、SP等就有了正确的值。
(12) 然后,内核跳转到PC指明的地址。这就是要执行的程序中第一个可执行指令的地址。现在开始执行"ls"这样的新程序。 内核从步骤(5)中存储的预先确定的区域中得到参数,然后生成所需的输出。如果子进程在前台执行,父进程会一直等到子进程终止;否则它会继续执行。
(13) 子进程终止,进入僵尸状态,期望使用的程序已经完成。现在,内核向父进程发送信号,指明"子进程死亡",这样现在就可以唤醒父进程了。
如果这个子进程打开新文件,那么这个子进程的用户文件描述符表、打开文件列表和inode表结构就和父进程的不同。如果该子进程调用另一个子程序,就会重复执行/分支进程。这样就会创建不同深度层次的进程结构。
4. 总结——“Linux内核装载:动态链接和静态链接”
- 静态链接库创建:
gcc -c fun.c
ar cqs libfun.a fun.o
gcc call.c -static -L. -lfun -o fun_static_call
./fun_static_call
- 动态链接库创建:
/*生成动态链接库*/
gcc fun.c -fPIC -shared -o libfun.so
/*-L指定查找动态链接库的路径,-lfun实际就是查找libfun.so*/
- 当ELF被load_elf_binary()装载完成后,函数返回至do_execve()在返回至sys_execve()。 ELF可执行文件的入口点取决于程序的链接方式:
1. 静态链接:elf_entry就是指向可执行文件里边规定的那个头部,即main函数处。
2. 动态链接:可执行文件是需要依赖其它动态链接库,elf_entry就是指向动态链接器的起点。