(2023-2024-1)20232830《Linux内核原理分析与设计》第八周作业

本文详细介绍了Linux内核的编译链接过程,包括预处理、编译、汇编和链接,以及ELF可执行文件格式。重点讲解了如何使用exec函数加载可执行程序和通过gdb进行调试,同时讨论了静态链接与动态链接的区别。
摘要由CSDN通过智能技术生成

(2023-2024-1)20232830《Linux内核原理分析与设计》第八周作业

1. 实验相关知识

1.1 编译链接的过程

编译链接是将源代码转换为可执行程序的过程,它通常包括以下几个步骤:

  1. 预处理(Preprocessing)
    预处理是编译链接过程的第一步。在这个阶段,预处理器会对源代码进行处理,包括展开宏定义、插入头文件内容等。预处理器根据以字符 ‘#’ 开头的预处理指令(如#include、#define等)修改源代码,生成一个经过预处理的代码文件。
  2. 编译(Compilation)
    编译是将预处理后的代码文件转换为汇编代码的过程。编译器会将源代码转换为汇编语言,生成相应的汇编代码文件。在这个过程中,编译器会进行词法分析、语法分析、语义分析等操作,检查代码的正确性,并将其转化为汇编代码。
  3. 汇编(Assembly)
    汇编是将汇编代码转换为机器代码的过程。汇编器会读取汇编代码文件,将其转换为机器可执行的指令,生成目标文件。每条汇编指令通常对应一条机器指令,包括操作码、寄存器、内存地址等。
  4. 链接(Linking)
    链接是将多个目标文件和库文件合并成一个可执行程序的过程。在链接过程中,链接器会解析目标文件中的符号引用和定义,处理符号表,将各个目标文件之间的引用关系进行连接。链接器还会处理库文件,将需要的库函数与程序进行关联。最终生成一个完整的可执行程序文件。

总结:编译链接的过程包括预处理、编译、汇编和链接。预处理将源代码进行宏展开和头文件插入;编译将源代码转换为汇编代码;汇编将汇编代码转换为机器代码;链接将目标文件和库文件合并成一个可执行程序。编译链接的过程将源代码转化为可执行程序,使得程序可以在计算机上运行。

1.2 ELF 可执行文件格式

ELF可执行文件格式由以下几个主要组成部分构成:

  1. ELF文件头(ELF Header)
    ELF文件头位于文件的开头,包含了描述整个文件的基本信息,如文件类型、目标体系结构、入口点地址、程序头表和节头表的偏移等。文件头提供了读取和解析ELF文件的基本信息。
  2. 程序头表(Program Header Table)
    程序头表描述了ELF文件在内存中的布局,包括加载和执行所需的段(段是ELF中的逻辑组织单位)信息。每个程序头表项描述了一个段的起始地址、大小、访问权限等信息,用于操作系统加载和执行可执行文件。
  3. 节头表(Section Header Table)
    节头表包含了关于各个节的信息,如代码段、数据段、符号表等。每个节头表项描述了一个节的起始地址、大小、访问权限、符号表索引等信息。节头表对于调试和链接程序非常重要。
  4. 节区(Sections)
    节区是ELF文件中的逻辑组织单位,包含各种数据和代码。常见的节区有代码段(.text)、数据段(.data)、只读数据段(.rodata)、符号表(.symtab)等。每个节区可以有不同的属性,如可执行、可写、可读等。
  5. 符号表(Symbol Table)
    符号表存储了程序中定义和引用的符号信息,如变量、函数、全局变量等。符号表中的每个符号项包含了符号的名称、类型、大小、绑定等信息,用于链接和调试程序。

除了上述主要组成部分,ELF文件还包含其他一些辅助信息,如动态链接信息、重定位表、调试信息等,用于支持动态链接、程序调试和代码重定位等功能。

ELF可执行文件格式的设计使得它具有良好的可扩展性和可移植性,能够适应不同的操作系统和目标体系结构。它被广泛应用于各种编程环境和开发工具中,为程序的开发、编译、链接和执行提供了基础支持。

2. 实验七:装载和启动可执行程序

2.1 使用“exec库函数”加载一个可执行文件

打开终端,执行以下命令,使得test.c文件中新增“exec”函数;

cd LinuxKernel
rm -rf menu
# 若无法克隆,可使用侧边栏上传文件压缩包形式将menu文件夹替换
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
vi test.c

打开test.c文件可以看到,主函数新增了exec命令;
exec
进入menu文件夹,并运行编译,查看exec函数实现;
make rootfs
exec

exec函数内容如下:

int Exec(int argc, char *argv[])
{
	int pid;
	/* fork another process */
	pid = fork();
	if (pid < 0) 
	{ 
		/* error occurred */
		fprintf(stderr,"Fork Failed!");
		exit(-1);
	} 
	else if (pid == 0) 
	{
		/*	 child process 	*/
    	printf("This is Child Process!\n");
		execlp("/hello","hello",NULL);
	} 
	else 
	{ 	
		/* 	parent process	 */
    	printf("This is Parent Process!\n");
		/* parent will wait for the child to complete*/
		wait(NULL);
		printf("Child Complete!\n");
	}
}

可以看到,exec函数使用了fork()和execlp()等系统调用。

  1. 首先,函数定义了一个名为Exec的函数,它接受两个参数:argc表示命令行参数的数量,argv是一个指向参数字符串数组的指针。
  2. 函数中使用了fork()系统调用来创建一个新的进程。fork()会复制当前进程,创建一个新的子进程。在原始进程中,fork()返回子进程的进程ID(pid),而在子进程中,fork()返回0。
  3. 通过检查fork()的返回值,可以确定当前代码是在父进程还是子进程中执行。
  4. 如果fork()返回值小于0,表示创建子进程失败,程序会打印错误信息并退出。
  5. 如果fork()返回值等于0,表示当前代码在子进程中执行。子进程会输出一条信息 “This is Child Process!,然后使用execlp()系统调用执行名为”/hello"的可执行文件。
  6. 如果fork()返回值大于0,表示当前代码在父进程中执行。父进程会输出一条信息 “This is Parent Process!”,然后使用wait()系统调用等待子进程的结束。
  7. 当子进程执行完毕后,父进程会继续执行,并输出一条信息 “Child Complete!”。

总体来说,这个函数创建了一个子进程,并在子进程中执行了一个名为"/hello"的程序。父进程等待子进程执行完毕后才继续执行。

2.2 通过gdb进行跟踪分析

回退到父目录,使用下面的命令启动内核并在CPU运行代码前停下以便调试;
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
qemu

打开一个新的终端窗口,依次使用下面的命令启动gdb调试;

gdb
file linux-3.18.6/vmlinux
target remote:1234

在系统调用sys_execve的入口处设置断点;
b sys_execve
继续运行程序,在QEMU窗口中输入exec,系统就会停在上面设置的断点处,如图所示:
gdb
继续设置以下断点,可完整跟踪进程的创建和启动代码,当然也可进行单步跟踪;

b load_elf_binary
b start_thread

gdb

2.3 实验相关问题回答

1. 新的可执行程序是从哪里开始执行的?
新的可执行程序从其入口点开始执行。入口点是程序的起始执行位置,通常是可执行文件的起始地址。

2. 为什么 execve 系统调用返回后新的可执行程序能顺利执行?
execve系统调用会将当前进程的内存映像替换为新的可执行文件的内容。这意味着原始的程序代码、数据和堆栈都会被新的可执行文件所替代,使得新的程序从其入口点开始执行。execve系统调用还会将命令行参数传递给新的程序,使其能够获取参数并进行相应的处理。

3. 对于静态链接的可执行程序和动态链接的可执行程序 execve 系统调用返回时会有什么不同?
对于静态链接的可执行程序,execve系统调用返回时不会有太大的区别。因为静态链接的可执行程序在编译时已经将所有需要的库函数和代码静态地链接到可执行文件中,所以在执行时不需要进一步的动态链接操作。
而对于动态链接的可执行程序,execve系统调用返回时会有一些不同。动态链接的可执行程序在运行时仍然依赖于共享库文件,而这些共享库文件通常在系统中以动态链接库的形式存在。当execve系统调用返回时,操作系统会根据可执行程序的需要加载所需的共享库文件,并建立程序与库之间的链接关系。这样,在程序执行过程中,当需要调用共享库中的函数时,会通过动态链接的方式在运行时解析并执行相应的函数。因此,动态链接的可执行程序在execve系统调用返回后还需要进行一些动态链接的操作,以确保能够正常调用和执行共享库中的代码。

3. 实验总结

在Linux中,exec函数族的作用是根据指定的文件名找到可执行文件,并用它来替换调用进程的内容。这意味着当前进程的可执行文件被完全替换为新的可执行文件。可执行文件的开始执行起点是根据执行execve系统调用时压入内核堆栈的EIP寄存器的值来确定的。尽管进程的可执行文件已经被替换,但实际开始执行新的可执行文件中的指令需要等到执行新程序定义的入口地址位置,通常是0x8048xx。通过修改内核堆栈中EIP寄存器的值,将其设置为新程序的起点,使得execve系统调用返回到用户态时可以开始执行新程序。

这里的可执行文件可以是二进制文件,也可以是任何Linux下可执行的脚本文件。如果不是可执行文件,内核会将其解释为一个shell文件,并由shell来执行。当Linux内核或程序使用fork函数创建子进程后,子进程通常会调用exec函数中的一种,以执行另一个程序。调用exec函数后,当前进程执行的程序完全被新程序替换,新程序会从其main函数处开始执行。由于exec函数并不创建新进程,因此进程ID并未改变,可以说exec函数只是用一个全新的程序替换了当前进程的代码、数据段和堆栈段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值