文章目录
一、Linux 内核如何装载和启动一个可执行程序
实验内容:
1、理解编译链接的过程和 ELF 可执行文件格式;
2、编程使用 exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式;
3、使用 gdb 跟踪分析一个 execve 系统调用内核处理函数 sys_execve ,验证您对 Linux 系统加载可执行程序所需处理过程的理解,详细内容参考本周第三节;推荐在实验楼 Linux 虚拟机环境下完成实验。
4、特别关注新的可执行程序是从哪里开始执行的?为什么 execve 系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序 execve 系统调用返回时会有什么不同?
5、分析 exec* 函数对应的系统调用处理过程。
二、实验过程
1.理解编译链接的过程和 ELF 可执行文件格式
编译链接的过程:
Linux中的编译链接过程通常包括预处理、编译、汇编、链接。
预处理:在编译之前进行,处理#include、#define等预处理指令,生成一个扩展后的源文件。
编译:将扩展后的源文件转换为汇编文件。编译器会将源代码翻译为汇编语言。
汇编:将汇编文件转换为目标文件。汇编器将汇编文件翻译成机器可以执行的二进制代码。
链接:将目标文件与库文件链接成可执行文件。链接器负责解析不同目标文件之间的符号依赖,并将它们合并成一个可执行文件。这包括全局符号解析、重定位和符号解析。
链接步骤中的可执行文件通常采用ELF(Executable and Linkable Format)格式。ELF包含文件头、节头表和节内容。具体如下:
ELF(Executable and Linkable Format)是一种二进制文件格式,用于可执行文件、目标文件和共享库。它包含文件头、节头表和节内容。在Linux系统中,ELF文件格式用于存储可执行文件的结构信息,它有助于操作系统理解如何加载和执行这些文件。这个格式描述了可执行文件的布局,其中:
①文件头(ELF Header):包含有关文件整体信息的描述,比如架构、字节序、文件大小等。它还包含有关程序入口点、节头表的偏移和节头表中条目的数量等重要信息。
②节头表(Section Header Table):包含有关文件中每个节(Section)的元数据,如段名、大小、类型、偏移等信息。
③节内容:包含实际的代码和数据。这些节包括代码段(text)、数据段(data)、bss段等。代码段存储执行指令,数据段存储变量和初始化数据,bss段存储未初始化的全局变量。
下面是一个展示EFL格式的代码:
readelf -h 你的可执行文件名
在这里,我编写了一个简单的C语言程序test.c,内容如下:
#include<stdio.h>
int main(){
printf("my StudentId is 20232831\n");
return 0;
}
编译运行后以及该文件的ELF格式结果如下:
2.编程使用 exec*库函数加载一个可执行文件,编程练习动态链接库的两种使用方式
①编程使用 exec*库函数加载一个可执行文件
exec*函数族涵盖了一系列函数,其中包括:execl、execle、execlp、execv、execve、execvp、execvpe。
这些函数允许你加载一个新的程序并执行它,允许传递参数列表和环境变量。每个函数都有特定的用途和参数列表。
这里写了两个C语言代码,进行使用exec*库的使用(这里用的是execve函数)。
execve 函数被用于装载并执行新的程序,它允许传递参数列表给新程序,并可以指定环境变量。
以下代码打印出了传递给它的命令行参数列表。其中,myecho.c打印出传递给它的命令行参数列表,myexecve.c 使用 execve 函数调用来加载一个新的程序,即myecho,它的名字(myecho)作为 myexecve 的第一个参数传入,接着便是list1[]中的其他数据被打印。
// myecho.c:
//打印命令行参数
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc,char*argv[]){
for(int i=0;i<argc;i++){
printf("argv[%d]:%s\n",i,argv[i]);
}
exit(EXIT_SUCCESS);
}
// myexecve.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(int argc,char* argv[]){
//这里的结尾必须加NULL 否则调用execve会报Bad address错误
char*list1[]={NULL,"Hello","Linux","World",NULL};
char*list2[]={NULL};
//参数数量必须为2 不为2报错
if(argc!=2){
fprintf(stderr,"%s wrong",argv[0]);
exit(EXIT_FAILURE);
}
//将list1第一个参数放为刚刚的那个echo文件的文件名
list1[0]=argv[1];
//execve
execve(argv[1],list1,list2);
perror("execve");
}
运行结果如下:
②动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式
首先,创建一个共享库,用于被这2种方法调用。再创建两个C语言代码,分别实现可执行程序装载时动态链接和运行时动态链接。
shared_lib.c是一个共享代码库,用于被调用链接。
main_exec_link_time.c 是使用可执行程序装载时的方法实现动态链接。
main_run_time.c 是使用运行时动态链接方法实现动态链接。
三个文件的代码如下:
// shared_lib.c - 共享库代码
#include <stdio.h>
void my_function() {
printf("动态共享链接库成功被调用!\n");
}
// main_exec_link_time.c - 使用可执行程序装载时动态链接
#include <stdio.h>
extern void my_function(); // 引用共享库中的函数
int main() {
printf("该方法是——使用可执行程序装载时动态链接!\n");
my_function();
return 0;
}
// main_run_time.c - 使用运行时动态链接
#include <stdio.h>
#include <dlfcn.h>
int main() {
void *handle;
void (*my_function)();
handle = dlopen("shared_lib.so", RTLD_LAZY); // 指定共享库的路径
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}
printf("该方法是——使用运行时动态链接!\n");
// 获取共享库中的函数指针
my_function = dlsym(handle, "my_function");
my_function(); // 执行共享库中的函数
dlclose(handle); // 关闭共享库
return 0;
}
首先,先编译shared_lib.c共享库代码成为共享库shared_lib.so,使用以下代码:
gcc -shared -o shared_lib.so -fPIC shared_lib.c
其次,使用以下代码编译并实现可执行程序装载时动态链接:
特别注意,export LD_LIBRARY_PATH=$PWD是环境变量,如果能通过编译却执行报错就必须加这一行,原因如下:
“export LD_LIBRARY_PATH=$ PWD”
这个命令会设置环境变量 LD_LIBRARY_PATH 为当前工作目录,用于指定动态链接器的库搜索路径。当程序在运行时需要动态链接共享库时,它会查找 LD_LIBRARY_PATH 中指定的路径,其中包含了动态链接库(.so 文件)的位置。将其设置为当前工作目录($PWD)会让动态链接器搜索并加载当前工作目录中的共享库。
export LD_LIBRARY_PATH=$PWD
gcc main_exec_link_time.c -o exec_link_time -L. -lshared_lib
./exec_link_time
最后,使用以下代码编译并实现运行时动态链接
gcc main_run_time.c -o run_time -ldl
./run_time
整个运行过程的截图如下:
3.使用 gdb 跟踪分析一个 execve 系统调用内核处理函数 sys_execve
①基础搭建部分
与上周实验一样,先完成以下代码,实现test.c文件的更换,即换成test_exec.c
cd LinuxeKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs
MenuOS>>help
MenuOS>>exec
以下是exec函数的具体代码:
打开一个冻结内核,再打开一个shell进行gdb分析调试,操作与前几次实验一致
cd LinuxKernel
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S //冻结内核的启动
新打开一个空shell,进入gdb调试,并建立连接:
cd LinuxKernel
gdb
(gdb)file linux-3.18.6/vmlinux
(gdb)target remote:1234
冻结的内核开始运行后,能使用exec,基础部分完成。
②gdb调试部分
分别对三个系统调用函数sys_execve、load_elf_binary、start_thread设置断点,进行gdb分析,分析过程如下:
gdb跟踪分析结束。
4.详细分析(4、5问)
1、特别关注新的可执行程序是从哪里开始执行的?为什么 execve 系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序 execve 系统调用返回时会有什么不同?
①新的可执行程序从可执行程序的入口点开始执行,该入口点是通常是ELF可执行文件格式的文件头,这里存储着程序入口点的地址。
①文件头(ELF Header):包含有关文件整体信息的描述,比如架构、字节序、文件大小等。它还包含有关程序入口点、节头表的偏移和节头表中条目的数量等重要信息。
当执行 execve 系统调用加载新的可执行程序时,操作系统会在进程的虚拟地址空间中将该程序加载至内存,并设置程序计数器(PC)指向可执行文件的入口点地址。此后,CPU 会从该地址处开始执行指令,进而启动程序的执行过程。
②为什么 execve 系统调用返回后新的可执行程序能顺利执行?
根据ChatGpt进行了以下回答:
execve 系统调用将创建一个全新的程序上下文,使得新程序能够独立地运行,而不会受到调用它的进程的影响。这种方式保证了程序的独立性,让新程序能够在自己的执行环境中顺利运行,同时保持了父进程的完整性。因此execve 系统调用返回后新的可执行程序能顺利执行。
③静态链接的可执行程序和动态链接的可执行程序 execve 系统调用返回时会有什么不同:
静态链接的程序在 execve 返回时会将整个程序及其所有依赖加载到内存中,而动态链接的程序则需要等到运行时才会根据需要加载动态链接库。因此,静态链接占用的空间也更大,因为加载了所有指明的库,而动态链接更灵活,但部署时需要做更多事。
以下是它们的优缺点:
2、分析 exec* 函数对应的系统调用处理过程。
exec* 函数通过在调用进程的上下文中执行新的程序文件来替换当前程序的映像。
调用exec*函数后,主要有以下系统调用:
execve 系统调用:在内核中执行一个新程序。
do_execve 函数:内核的主要执行路径,处理用户空间传来的程序路径、参数和环境变量。
load_elf_binary 函数(对于 ELF 格式的程序):加载可执行文件,并设置新进程的地址空间和资源。
start_thread:在用户空间开始执行新程序。
整个流程包括加载可执行文件、建立新进程的地址空间和资源,以及开始执行新程序。这确保了新程序的ELF 文件能被正确加载和执行。
而本文章中使用的execve系统调用的大致过程如下:
1、用户态到内核态:首先,进程从用户态切换到内核态,触发系统调用。
2、参数解析:内核获取到 execve 中传递的参数,包括新程序的路径和参数列表。
3、打开文件:内核根据给定的路径,尝试打开这个可执行文件。
4、权限检查:内核会进行一系列的权限和安全检查,确保调用进程有足够的权限来执行这个文件。
5、替换当前进程:如果权限检查通过,内核将当前进程的映像替换为新程序的映像。这意味着当前进程的代码、数据和堆栈都会被新程序的内容替换掉。
6、加载新程序:内核将新的可执行文件加载到内存,并设置一个新的用户空间。
7、开始执行:内核将控制权移交给新程序的入口点,开始执行新程序。
三、Chatgpt帮助
总结
本次实验内容为“Linux 内核如何装载和启动一个可执行程序”,其包含了编译链接、动态链接、exec*库函数及其系统调用等等内容。通过练习、调试和分析,我深入理解了可执行程序装载、运行时动态链接的过程,使用GDB跟踪sys_execve系统调用函数,也加深了我对Linux系统加载程序的理解。