20252803《Linux内核原理与分析》第八周作业
阅读学习教材「庖丁解牛Linux 操作系统分析 」第8章,有问题优先使用chatgpt等AI工具。或者到蓝墨云班课中提问,24小时内回复,鼓励解答别人问题,提问前请阅读「如何提问」。教材深入学习关注豆列「Linux内核及安全」。学习蓝墨云班课中第八周视频「Linux内核的实质和Linux系统的一般执行过程」,并完成实验楼上配套实验七。,注意从下往上看。基于树莓派或其他平台完成ARM相关内容。
作业要求
阅读学习教材「庖丁解牛Linux 操作系统分析 」第8章,有问题优先使用chatgpt等AI工具。或者到蓝墨云班课中提问,24小时内回复,鼓励解答别人问题,提问前请阅读「如何提问」。
教材深入学习关注豆列「Linux内核及安全」。
学习蓝墨云班课中第八周视频「Linux内核的实质和Linux系统的一般执行过程」,并完成实验楼上配套实验七。,注意从下往上看。基于树莓派或其他平台完成ARM相关内容。
作业标题 “学号《Linux内核原理与分析》第X周作业”,重点是遇到的问题和解决方案内容涵盖教材学习和视频,提交格式用Markdown,同时提交转换的 PDF(VSCode 有相关插件)。
实验七:Linux 内核如何装载和启动一个可执行程
1、理解编译链接的过程和 ELF 可执行文件格式,详细内容参考本周第一节;
编程使用 exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式,详细内容参考本周第二节;
2、使用 gdb 跟踪分析一个 execve 系统调用内核处理函数 sys_execve ,验证您对 Linux 系统加载可执行程序所需处理过程的理解,详细内容参考本周第三节;推荐在实验楼 Linux 虚拟机环境下完成实验。
3、特别关注新的可执行程序是从哪里开始执行的?为什么 execve 系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序 execve 系统调用返回时会有什么不同?
1、理解编译链接的过程和 ELF 可执行文件格式
(1)编译链接是将源代码转化为可执行文件的完整流程,主要分为四个阶段:
预处理(Preprocessing):负责处理源代码中的预处理指令,如#include(引入头文件)、#define(宏替换)、#ifdef(条件编译)等。此阶段会展开宏定义、合并头文件内容,并生成预处理后的代码文件(通常以.i为扩展名)。例如,#include <stdio.h>会将标准输入输出库的内容插入到当前文件中。
编译(Compilation):将预处理后的代码(.i文件)转换为汇编语言代码(.s文件)。编译器会进行语法分析、语义分析、优化(如常量折叠、死代码删除)等操作,最终生成与机器架构相关的汇编指令。例如,C 语言中的int a = 1 + 2;会被转换为对应的汇编加法指令。
汇编(Assembly):将汇编代码(.s文件)转换为二进制机器码,生成目标文件(.o或.obj,在 Linux 中为 ELF 格式)。目标文件包含二进制指令、数据以及符号表(记录函数和变量的名称与地址),但尚未解决跨文件的符号引用(如调用其他文件中的函数)。
链接(Linking):由链接器将多个目标文件(及可能的库文件)合并为一个可执行文件。此阶段会解决符号引用(将函数调用与实际定义的地址关联)、分配内存地址,并生成最终的 ELF 可执行文件。链接分为静态链接(将库代码直接嵌入可执行文件)和动态链接(运行时加载共享库)。
(2)ELF 可执行文件格式说明
ELF(Executable and Linkable Format)是 Linux/Unix 系统中标准的二进制文件格式,适用于可执行文件、目标文件、共享库等。其核心结构包括:
ELF 头部(ELF Header):位于文件起始位置,是 ELF 文件的 “身份证”。包含文件类型(如可执行文件、目标文件、共享库)、机器架构(如 x86、ARM)、入口地址(程序开始执行的内存地址)、节区表和程序头表的偏移量等关键信息。通过readelf -h命令可查看。
节区表(Section Header Table):描述文件中所有 “节区”(Section)的元数据(名称、偏移量、大小、权限等)。常见节区包括:
.text:存放二进制指令(代码段),具有只读和可执行权限。
.data:存放已初始化的全局变量和静态变量,具有读写权限。
.bss:存放未初始化的全局变量和静态变量(仅占符号表项,不占用文件磁盘空间)。
.symtab:符号表,记录函数、变量的名称、类型、地址等信息。
.strtab:字符串表,存储符号表中用到的字符串(如函数名、变量名)。
程序头表(Program Header Table):仅存在于可执行文件和共享库中,描述如何将文件加载到内存。将连续的节区合并为 “段”(Segment),并指定段的虚拟地址、大小、权限(如只读、读写、可执行)等。操作系统加载程序时,会根据程序头表分配内存并映射文件内容。
下面是简单的C语言程序test.c案例,内容如下:
#include<stdio.h>
int main(){
printf("my StudentId is 20252803\n");
return 0;
}

下面代码可以展示EFL格式:
readelf -h 待执行文件名
编译运行后展示该文件的ELF格式结果如下:

2、编程使用 exec*库函数加载一个可执行文件,编程练习动态链接库的两种使用方式
(1)编程使用 exec*库函数加载一个可执行文件
exec*函数族涵盖了一系列函数,其中包括:execl、execle、execlp、execv、execve、execvp、execvpe。
这些函数允许你加载一个新的程序并执行它,允许传递参数列表和环境变量。每个函数都有特定的用途和参数列表。
这里写了两个C语言代码,使用execve函数。
execve 函数被用于装载并执行新的程序,它允许传递参数列表给新程序,并可以指定环境变量。
以下代码打印出了传递给它的命令行参数列表。其中,myecho.c打印出传递给它的命令行参数列表,myexecve.c 使用 execve 函数调用来加载一个新的程序,即myecho,它的名字(myecho)作为 myexecve 的第一个参数传入,接着便是list1[]中的其他数据被打印。
#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);
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(int argc,char* argv[]){
char*list1[]={NULL,"Hello","Linux","World",NULL};
char*list2[]={NULL};
if(argc!=2){
fprintf(stderr,"%s wrong",argv[0]);
exit(EXIT_FAILURE);
}
list1[0]=argv[1];
execve(argv[1],list1,list2);
perror("execve");
}





(2)动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式
首先,创建一个共享库,用于被这2种方法调用。再创建两个C语言代码,分别实现可执行程序装载时动态链接和运行时动态链接。
shared_lib.c是一个共享代码库,用于被调用链接。
#include <stdio.h>
void my_function() {
printf("excute success\n");
}
main_exec_link_time.c 是使用可执行程序装载时的方法实现动态链接(loading)
#include <stdio.h>
extern void my_function();
int main() {
printf("This method is loading\n");
my_function();
return 0;
}
main_run_time.c 是使用运行时动态链接方法实现动态链接(running)
#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("The method is running\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
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
(1)搭建基础部分
cd LinuxeKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs

这是test.c中的exec函数具体代码:
(2)在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

gdb调试操作
分别对三个系统调用函数sys_execve、load_elf_binary、start_thread设置断点,进行gdb分析,过程如下:



四、问题详细分析
1、特别关注新的可执行程序是从哪里开始执行的?
新的可执行程序从可执行程序的入口点开始执行,该入口点是通常是ELF可执行文件格式的文件头,这里存储着程序入口点的地址。
当执行 execve 系统调用加载新的可执行程序时,操作系统会在进程的虚拟地址空间中将该程序加载至内存,并设置程序计数器(PC)指向可执行文件的入口点地址。此后,CPU 会从该地址处开始执行指令,进而启动程序的执行过程。
2、为什么execve系统调用返回后新的可执行程序能顺利执行?

3、静态链接的可执行程序和动态链接的可执行程序 execve 系统调用返回时会有什么不同
加载内容:静态链接程序仅加载自身;动态链接程序还需加载依赖的动态库。
入口执行:静态直接进入自身启动代码;动态先运行动态链接器(ld.so)处理库解析,再进入程序逻辑。
启动速度:静态更快(无额外步骤);动态较慢(需解析库)。
环境依赖:静态不依赖系统库;动态依赖系统中存在匹配的动态库。
五、分析 exec* 函数对应的系统调用处理过程

更多推荐



所有评论(0)