Linux 内核如何装载和启动一个可执行程序
一、理论知识
1、理解编译链接的过程和ELF可执行文件格式
1.预处理、编译、链接:
1)预处理,处理代码中的宏定义和 include 文件,并做语法检查
gcc -E hello_world.c -o hello_world.i
2)编译,生成汇编代码
gcc -S hello_world.i -o hello_world.s
3)汇编,生成 ELF 格式的目标代码
gcc -c hello_world.s -o hello_world.o
4)链接,生成可执行代码
gcc hello_world.o -o hello_world
5)执行程序
./hello_world hello, world!
该过程总结如下图所示
2.ELF文件格式
ELF 格式:可执行和可链接(Executable and Linkable Format) 是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式。
可重定位文件,如:.o 文件,包含代码和数据,可以被链接成可执行文件或共享目标文件,静态链接库属于这类。 可执行文件,如:/bin/bash
文件,包含可直接执行的程序,没有扩展名。
共享目标文件,如:.so文件,包含代码和数据,可以跟其他可重定位文件和共享目标文件链接产生新的目标文件,也可以跟可执行文件结合作为进程映像的一部分。
ELF 文件由 ELF header 和文件数据组成,文件数据包括:
Program header table, 程序头:描述段信息 .text, 代码段:保存编译后得到的指令数据 .data,
数据段:保存已经初始化的全局静态变量和局部静态变量 Section header table, 节头表:链接与重定位需要的数据
二、实验过程
1、编程使用 exec*库函数加载一个可执行文件
动态链接分为可执行程序装载时动态链接和运行时动态链接,如下代码演示了这两种动态链接。
1)运行时动态连接方式
#include <stdio.h>
#include <unistd.h>
int main() {
char *args[] = {"ls", "-l", NULL}; // 用于传递给被执行程序的命令行参数
// 使用execvp执行ls命令
execvp("ls", args);
// 如果execvp失败,会执行以下代码
perror("execvp"); // 打印错误信息
return 1;
}
2)加载动态链接库方式
#include <stdio.h>
#include <dlfcn.h>
int main() {
// 打开动态链接库
void *handle = dlopen("./mylib.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen() error: %s\n", dlerror());
return 1;
}
// 获取共享库中的函数指针
void (*my_function)() = dlsym(handle, "my_function");
if (!my_function) {
fprintf(stderr, "dlsym() error: %s\n", dlerror());
return 1;
}
// 调用共享库中的函数
my_function();
// 关闭共享库
dlclose(handle);
return 0;
}
2、使用 gdb 跟踪分析一个 execve 系统调用内核处理函数 sys_execve
打开实验楼中的虚拟机,在shell中依次运行以下命令,获取本次实验的代码,并编译运行
cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs
效果如下
然后启动调试内核:
再开一个shell,打开gdb,并加载符号表,连接到端口1234:
然后在gdb,中设置断点:这里我设置了sys_execve,load_elf_binary,
start_thread三处断点。
然后在menu界面中输入命令:exec ,触发了断点。
发现程序第一个断点停在了sys_execve处:
按s,单步执行,发现接下来会运行do_execve:
按c继续执行,发现接下来停在了load_elf_binary处:
使用命令po new_ip 查看new_ip的值,它等于0x8048d0a
三、实验总结
调用execve的可执行程序时,系统调用exceve陷入内核,这时会创建一个新的用户态堆栈,实际是把命令行参数的内容和环境变量的内容通过指针的方式传递给系统调用内核处理函数的,然后内核处理函数在创建可执行程序新的用户态堆栈的时候,会把这些拷贝到用户态堆栈初始化新的可执行程序的执行上下文环境(先函数调用参数传递,再系统调用参数传递)。