-
编译链接的过程和ELF可执行文件格式(以x86为例)
shiyanlou:~/ $ cd Code [9:27:05]
2.shiyanlou:Code/ $ vi hello.c [9:27:14]
3.shiyanlou:Code/ $ gcc -E -o hello.cpp hello.c -m32 [9:34:55] //预处理中间文件,包括加载include文件,宏替换等工作
4.shiyanlou:Code/ $ vi hello.cpp [9:35:04]
5.shiyanlou:Code/ $ gcc -x cpp-output -S -o hello.s hello.cpp -m32 [9:35:21] //编译成汇编代码.s
6.shiyanlou:Code/ $ vi hello.s [9:35:28]
7.shiyanlou:Code/ $ gcc -x assembler -c hello.s -o hello.o -m32 [9:35:58] //编译成目标代码.o
8.shiyanlou:Code/ $ vi hello.o [9:38:44]
9.shiyanlou:Code/ $ gcc -o hello hello.o -m32 [9:39:37] //链接成可执行文件(使用共享库)
10.shiyanlou:Code/ $ vi hello [9:39:44]
11.shiyanlou:Code/ $ gcc -o hello.static hello.o -m32 -static [9:40:21] //静态链接成可执行文件(将标准库放入文件中)
12.shiyanlou:Code/ $ ls -l [9:41:13]
13.-rwxrwxr-x 1 shiyanlou shiyanlou 7292 3\u6708 23 09:39 hello
14.-rw-rw-r-- 1 shiyanlou shiyanlou 64 3\u6708 23 09:30 hello.c
15.-rw-rw-r-- 1 shiyanlou shiyanlou 17302 3\u6708 23 09:35 hello.cpp
16.-rw-rw-r-- 1 shiyanlou shiyanlou 1020 3\u6708 23 09:38 hello.o
17.-rw-rw-r-- 1 shiyanlou shiyanlou 470 3\u6708 23 09:35 hello.s
18.-rwxrwxr-x 1 shiyanlou shiyanlou 733254 3\u6708 23 09:41 hello.static
在静态链接中,我们由于将libC的相关内容直接加入了可执行程序中,所以test.static的大小要远大于动态链接的可执行程序。
目标文件(objectfile)即存放目标代码(编译器或汇编器处理源代码后所生成的代码,它一般由机器代码或接近于机器语言的代码组成)的计算机文件,它常被称作二进制文件(binaries),这种文件是体系结构相关的。Windows下的目标文件为PE,Linux下的目标文件为ELF(Excutable and Linking Format)格式文件。格式如下:
ELF头部格式
readelf可以查看elf文件的各种信息,你可以使用-a查看全部信息,也可以使用-h只查看头部信息,我们可以对照上面的结构体分析一下刚才用来做测试的test文件的头部信息。
chenxu@ ~/Code/kernel/lab7$ readelf -h test
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 // 魔数和相关信息
Class: ELF32 // 目标文件类型
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386 // 硬件体系
Version: 0x1 // 目标文件版本
Entry point address: 0x80483a0 // 程序进入点
Start of program headers: 52 (bytes into file) // 程序头部偏移量
Start of section headers: 6188 (bytes into file) // 节头部偏移量
Flags: 0x0 // 处理器特定标志
Size of this header: 52 (bytes) // ELF头部长度
Size of program headers: 32 (bytes) // 程序头部中一个条目的长度
Number of program headers: 9 // 程序头部条目个数
Size of section headers: 40 (bytes) // 节头部中一个条目的长度
Number of section headers: 30 // 节头部条目个数
Section header string table index: 27 // 节头部字符表索引
当程序要加载到内存中运行时(对于x86体系结构,进程有4G地址空间,1G为内核空间,3G为用户空间),将ELF文件的段(代码段、数据段)加载到进程的地址空间,ELF文件与进程虚拟空间有一个映射关系。默认加载ELF文件到0x8048000,因为不同ELF文件头部大小有所不同,所以对于启动一个刚加载过可执行文件的进程来说,开始执行的入口点即为ELF头文件中的Entry point address。
2.分析exec*函数对应的系统调用处理过程
我们一般是通过一个Shell程序来启动一个可执行程序的,一般来说Shell先fork出一个子进程,然后会调用execve将命令行参数和环境参数传递给可执行程序的main函数,库函数exec*都是execve的封装例程。
int execve(constchar * filename,char * const argv[ ],char * const envp[ ]); //这里可以加入命令行参数和环境变量,但具体是否使用环境变量和参数,取决于main函数的写法。
比如ls -l列出当前目录下的文件,Shell本身不限制命令行参数的个数,命令行参数的个数受限于命令自身,也就是我们写的main函数是否愿意接收参数。
int main(int argc, char *argv[], char *envp[])
execve系统调用对应的内核处理函数sys_execve的作用是:加载可执行文件覆盖原来进程的可执行文件,清空原来的用户态堆栈,通过指针将命令行和环境变量压入用户态堆栈。
sys_execve大致过程如下
do_execve ->do_execve_common->exec_binprm->search_binary_handler(寻找文件格式对应的解析模块)
在search_binary_handler中有一个重要的模块load_elf_binary,它负责解析elf文件,之后调用start_thread,修改内核堆栈中的EIP的值为elf_entry后调用iret返回用户态,
如果是静态链接:start_thread 把我们返回用户态的位置从int 0x80的下一条指令的位置变成新加载的可执行文件的entry位置(new_ip)。
sys_execve的内部处理过程
SYSCALL_DEFINE3(execve,const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
首先来看do_execve,其作用是修改参数和环境变量的数据结构,之后调用do_execve_common
int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execve_common(filename, argv, envp);
}
do_execve_common
int do_execve_common()
{
file = do_open_exec(filename); //打开要加载的可执行文件
...
retval = exec_binprm(bprm) ; //exec_binprm会调用search_binary_handler,在此都与静态链接相同
...
exec_binprm(bprm); //加载程序
}
search_binary_handler遍历链表来尝试加载目标文件,找到了则执行load_elf_binary
load_elf_binary()
{
...
解析ELF文件
...
elf_map(bprm->file, load_bias + vaddr,...) //把目标文件映射到地址空间中
...
if (elf_interpreter) //如果需要加载连接器(即如果是动态链接)
...
elf_entry = load_elf_interp(...) //加载连接器id,把elf_entry设置为动态链接器ld的起点
else //否则是静态链接,把目标文件的入口赋值给elf_entry
...
start_thread(..., elf_entry, ...); //对于动态链接,此时elf_entry指向动态链接器,此时将cpu控制权交给ID来加载依赖库来完成动态链接。对于静态链接,elf_entry是新程序执行的起点(不再是默认的0x8048000)
}
3.使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve
gdb的初始化操作在前几篇博客中已经多次提及,在此不再赘述。