第一部分 课本学习
- ELF文件(目标文件)格式主要三种:
1)可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。(目标文件或者静态库文件,即linux通常后缀为.a和.o的文件)
2)可执行文件:文件保存着一个用来执行的程序。(例如bash,gcc等)
3)共享目标文件:共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。(linux下后缀为.so的文件。)
- 一般的 ELF 文件包括三个索引表:
1)ELF header:在文件的开始,保存了路线图,描述了该文件的组织情况。
2)Program header table:告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
3)Section header table :包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。
- 从源代码到可执行程序所要经历的过程概述:
以hello world的.c文件为例进行分析,c语言代码如下:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("hello, world!
");
return 0;
}
编译步骤如下:
vi hello.c
gcc -E -o hello.cpp hello.c -m32 //对.c文件预处理
vi hello.cpp
gcc -x cpp-output -S -o hello.s hello.cpp -m32 //编译成汇编代码.s
vi hello.s
gcc -x assembler -c hello.s -o hello.o -m32 //汇编成目标代码.o是二进制的文件
vi hello.o
gcc -o hello hello.o -m32 //链接成可执行文件
vi hello
gcc -o hello.static hello.o -m32 -static
- 静态链接和动态链接
静态链接:在编译链接时直接将需要的执行代码复制到最终可执行文件中,优点是代码的装载速度快,执行速度也比较快,对外部环境依赖度低。编译时它会把需要的所有代码都链接进去,应用程序相对比较大。缺点是如果多个应用程序使用同一库函数,会被装载多次,浪费内存。
动态链接:在编译时不直接复制可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统。操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库去执行代码,最终达到运行时链接的目的。优点是多个程序可以共享同一段代码,而不需要在磁盘上存储多个复制。缺点是在运行时加载,可能会影响程序的前期执行性能,而且对使用的库依赖性较高吗,在升级时特别容易出现版本不兼容的问题。
第二部分 使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve
- 实验步骤:
将menu目录删除,利用git命令克隆一个新的menu目录。
rm menu -rf
git clone https://github.com/mengning/menu.git
用test_exec.c将test.c覆盖,然后重新编译rootfs。可以发现,除了增加了ececlp函数外,还在Makefile中编译了hello.c,然后在生成根文件系统时把int和hello都放到rootfs.img中。在这个实验中,hello就是一个加载进来的可执行文件。
mv test_exec.c test.c
make rootfs
使用help命令可以看到增加了exec指令,执行exec指令发现比fork指令增加了一行输出“hello world!”。实际上是新加载了一个可执行程序来输出了一行语句。
启动内核到调试的状态,加载符号表并设置端口,准备单步调试。
Qemu -system-x86_64 -kernel bzImage -initrd /home/YL/rootfs.img -S -s file ../linux-3.18.6/vmlinux target remote:1234
启动新的终端窗口开始gdb调试,设置断点到“sys_exec” “load_elf_binary” “start_thread”。
b sys_exec
b load_elf_binary
b start_thread
进入“sys_exec”函数内部发现调用了“do_execve()”函数继续执行到“load_elf_binary”处的断点,此时调用这个函数进行对可执行文件格式的解析。
继续执行到“start_thread”处的断点,因为是静态链接,“elf_entry”指向了可执行文件中定义的入口地址,使用“po new_ip”指令打印其指向的地址,“new_ip”是返回到用户态的第一条指令的地址。查看hello的elf头部,查看定义的入口地址与“new_ip”所指向的地址是否一致。
使用readelf -h hello查看hello的elf头部,发现定义的入口地址与new_ip所指向的地址一致。
第三部分 总结
- 可执行文件开始执行的起点在修改调用execeve系统调用时压入内核堆栈的EIP寄存器的值,因为此时标志着当前进程的可执行文件已经被完全替换为新的可执行文件了,但实际开始执行可执行文件中的指令还需要等到执行文件中定义的入口地址的位置,一般地址为0x8048xxx的位置。通过修改内核堆栈中EIP寄存器的值作为新程序的起点,让execve系统调用返回到用户态时执行新程序。