2019-2020-1 20199315 《Linux内核原理与分析》 第八周作业

可执行程序工作原理

程序编译

Linux系统中c语言源程序的编译过程主要分为四个步骤:预处理、编译、汇编、链接。

预处理

预处理阶段的工作内容如下:

  • 删除所有注释;

  • 删除所有#define,并展开所有宏定义;

  • 处理所有条件预编译指令;

  • 处理#include预编译指令,将被包含的文件插入预编译指令所在位置;

  • 添加行号和文件名标识。

其在shell中的执行命令为

gcc -E hello.c -o hello.i

编译

编译阶段的主要功能是将预处理后的高级语言代码编译成汇编语言。

其在shell中的执行命令为

gcc -S hello.c -o hello.s -m32

汇编

汇编阶段的主要功能是将汇编语言代码汇编成二进制机器码。

其在shell中的执行命令为

gcc -c hello.c -o hello.o -m32

链接

链接阶段的主要功能是将各种代码和数据部分收集起来并组合成一个单一文件,这个文件可以被加载到内存中执行。

其在shell中的执行命令为

gcc hello.o -o hello -m32 -static

朴素的写了个helloworld做测试:

可执行程序——ELF文件

ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。

ELF文件的类型

ELF文件可分为三类:

  • 可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。(目标文件或者静态库文件,即linux通常后缀为.a和.o的文件)

  • 可执行文件:文件保存着一个用来执行的程序。(例如bash,gcc等)

  • 共享目标文件:共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。(linux下后缀为.so的文件。)

ELF文件格式

ELF文件的主体结构如下图所示:

ELF Header

ELF Header部分长64字节,包括了可执行文件类型(32位/64位),程序入口地址,其他结构部分起始地址、大小、数目等ELF文件的基本数据。

在shell中通过readelf -h hello命令,查看hello可执行ELF文件的ELF Header部分内容:

Section Header Table

Sections是在ELF文件中,用以装载内容数据的最小容器。在ELF文件里,每一个 sections内都装载了性质属性都一样的内容,如:

1) .text section 里装载了可执行代码;

2) .data section 里面装载了被初始化的数据;

3) .bss section 里面装载了未被初始化的数据;

4) 以 .rec 打头的 sections 里面装载了重定位条目;

5) .symtab 或者 .dynsym section 里面装载了符号信息;

6) .strtab 或者 .dynstr section 里面装载了字符串信息;

7) 其他还有为满足不同目的所设置的section,比方满足调试的目的、满足动态链接与加载的目的等等。

一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的Section Head Table(SHT)决定。在SHT中,针对每一个section,都设置有一个条目,用以描述对应的这个section,其内容主要包括该section的名称、类型、大小以及在整个ELF文件中的字节偏移位置等等。

在shell中通过readelf -S hello命令,查看hello可执行ELF文件的Section Head Table部分内容:

可以看到,正如Header中说明的,Section Head Table共有31个Section项。

其中,.symtabsection项存储了程序的字符串表,用readelf -s hello查看:

Type 列表示符号的种类,Bind 列表示符号的绑定类型,两者共同构成了 st_info 字段。

Program Header Table

在编译器进行链接步骤时,可重定位对象文件中的sections是作为输入,给链接器那去做链接用的,所以这些 sections 也经常被称做输入 section。

而链接器在链接可执行文件或动态库的过程中,会把来自不同可重定位对象文件中的相同名称的 section 合并起来构成同名的section。接着,其又会把带有相同属性(比方都是只读并可加载的)的 section 都合并成所谓 segments(段)。segments 作为链接器的输出,常被称为输出section。

一个单独的 segment 通常会包含几个不同的 sections,比方一个可被加载的、只读的segment 通常就会包括可执行代码section .text、只读的数据section .rodata以及给动态链接器使用的符号section .dymsym等等。section 是被链接器使用的,但是 segments 是被加载器所使用的。加载器会将所需要的 segment 加载到内存空间中运行。和用 sections header table 来指定一个可重定位文件中到底有哪些 sections 一样。在一个可执行文件或者动态库中,也需要有一种信息结构来指出包含有哪些 segments。这种信息结构就是 Program Header Table。

在shell中通过readelf -l hello命令,查看hello可执行ELF文件的Program Header Table部分内容:

Linux内核如何装载和启动一个可执行程

实验要求

理解编译链接的过程和ELF可执行文件格式,详细内容参考本周第一节;​

编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式,详细内容参考本周第二节;

使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解,详细内容参考本周第三节;推荐在实验楼Linux虚拟机环境下完成实验。

特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同?

实验过程

1.开始先更新内核,再用test_exec.c将test.c覆盖掉:

查看test.c文件,可以看到新增加了exec系统调用

e hello.c切换到hello.c

查看Makefile

make rootfs启动内核并验证execv函数

冻结内核,终端2在menu里启动GDB调试

先停在sys_execve处,再设置其它断点;按c一路运行下去直到断点sys_execve

new_ip是返回到用户态的第一条指令

退出调试状态,输入redelf -h hello可以查看hello的EIF头部

新开一个窗口,gdb进入内部可以发现正在修改内核堆栈

struct pt_regs *regs就是内核堆栈栈底的部分,发生中断的时候,esp和ip都进行压栈。通过修改内核堆栈中EIP的值(也就是把压入栈中的值用new_ip替换)作为新程序的起点。

实验分析

elf头部文件分析

前面调出了elf header文件,即

可见elf头大小为52字节,用dump命令16进制读取前52个字节进行分析

命令:hexdump -x hello -n 52

  • 第一行,对应e_ident[EI_NIDENT]。小端法实际表示内容为7f454c46010101000000000000000000,前四个字节为elf固定开头7f454c46(0x45,0x4c,0x46是'e','l','f'对应的ascii编码),表示这是一个ELF对象。接下来的一个字节01表示是一个32位对象,接下来的一个字节01表示是小端法表示,再接下来的一个字节01表示文件头版本。剩下的默认都设置为0.

  • 第二行,e_type值为0x0002,表示是一个可执行文件。e_machine值为0x0003,表示是intel80386处理器体系结构。e_version值为0x00000001,表示是当前版本。e_entry值为0x04080a8d,表示入口点。e_phoff值为0x00000034,表示程序头表的偏移量为0x34即52个字节刚好是elf头大小。

  • 第三行,e_shoff值为0x000a20f0,表示节头表的偏移地址。e_flags值为0x00000000,表示未知处理器特定标志。e_ehsize值为0x0034,表示elf文件头大小52个字节。e_phentsize表示一个程序头表中的入口(程序头)的长度,值为0x0020即32字节。e_phnum的值为0x0006,给出程序头表中的入口数目。e_shentsize值为0x0028表示节头表入口(节头)大小为40字节。

  • 第四行,e_shnum值为0x001f,表示节头表入口有31个。e_shstrndx值为0x001c,表示节名串表的在节表中的索引号。

exec()函数结构分析

int do_execve(struct filename *filename,
    const char __user *const __user *__argv,
    const char __user *const __user *__envp)
{
    return do_execve_common(filename, argv, envp);
}
 
 
static int do_execve_common(struct filename *filename,
                struct user_arg_ptr argv,
                struct user_arg_ptr envp)
{
    // 检查进程的数量限制
 
    // 选择最小负载的CPU,以执行新程序
    sched_exec();
 
    // 填充 linux_binprm结构体
    retval = prepare_binprm(bprm);
 
    // 拷贝文件名、命令行参数、环境变量
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    retval = copy_strings(bprm->envc, envp, bprm);
    retval = copy_strings(bprm->argc, argv, bprm);
 
    // 调用里面的 search_binary_handler
    retval = exec_binprm(bprm);
 
    // exec执行成功
 
}
 
static int exec_binprm(struct linux_binprm *bprm)
{
    // 扫描formats链表,根据不同的文本格式,选择不同的load函数
    ret = search_binary_handler(bprm);
    // ...
    return ret;
}

do_ execve调用了do_ execve_ common,而do_ execve_ common又主要依靠了exec_ binprm,在exec_ binprm中又有一个至关重要的函数,叫做search_binary_ handler。这就是sys_execve的内部处理过程。

遇到的问题

gdb里面第一条命令,提示我没有文件或目录

发现我是直接在LinuxKernel里面进的gdb,应该cd进入menu里面再gdb

此为本人Linux学习第七周的内容,如有不足,还请批评指正,不胜感激。

以上

猜你喜欢

转载自www.cnblogs.com/qianxiaoxu/p/11819722.html