CSAPP大作业

CSAPP大作业,按要求发布到公开自媒体。
由于实验报告和程序内容透露个人信息,本文将于该课成绩发布后删除。

摘 要
本文主要以hello程序为例,追溯从代码编写至链接完成,从程序加载至程序结束的整个过程,梳理在Linux系统下常规C语言程序的生成和执行过程。本文重点关注与计算机系统紧密相关的细节,对相关的现象进行分析和讨论,解释其与计算机系统相关机制的关系。

关键词:计算机系统;预处理;编译;汇编;链接;进程;虚拟内存;

目 录

第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处理 - 6 -
2.1 预处理的概念与作用 - 6 -
2.2在UBUNTU下预处理的命令 - 6 -
2.3 HELLO的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 8 -
3.4 本章小结 - 14 -
第4章 汇编 - 15 -
4.1 汇编的概念与作用 - 15 -
4.2 在UBUNTU下汇编的命令 - 15 -
4.3 可重定位目标ELF格式 - 15 -
4.4 HELLO.O的结果解析 - 19 -
4.5 本章小结 - 20 -
第5章 链接 - 21 -
5.1 链接的概念与作用 - 21 -
5.2 在UBUNTU下链接的命令 - 21 -
5.3 可执行目标文件HELLO的格式 - 21 -
5.4 HELLO的虚拟地址空间 - 27 -
5.5 链接的重定位过程分析 - 27 -
5.6 HELLO的执行流程 - 29 -
5.7 HELLO的动态链接分析 - 30 -
5.8 本章小结 - 31 -
第6章 HELLO进程管理 - 32 -
6.1 进程的概念与作用 - 32 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 32 -
6.3 HELLO的FORK进程创建过程 - 32 -
6.4 HELLO的EXECVE过程 - 32 -
6.5 HELLO的进程执行 - 32 -
6.6 HELLO的异常与信号处理 - 33 -
6.7本章小结 - 35 -
第7章 HELLO的存储管理 - 36 -
7.1 HELLO的存储器地址空间 - 36 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 36 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 36 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 36 -
7.5 三级CACHE支持下的物理内存访问 - 36 -
7.6 HELLO进程FORK时的内存映射 - 37 -
7.7 HELLO进程EXECVE时的内存映射 - 37 -
7.8 缺页故障与缺页中断处理 - 37 -
7.9动态存储分配管理 - 37 -
7.10本章小结 - 38 -
第8章 HELLO的IO管理 - 39 -
8.1 LINUX的IO设备管理方法 - 39 -
8.2 简述UNIX IO接口及其函数 - 39 -
8.3 PRINTF的实现分析 - 39 -
8.4 GETCHAR的实现分析 - 39 -
8.5本章小结 - 39 -
结论 - 40 -
附件 - 42 -
参考文献 - 43 -

第1章 概述
1.1 Hello简介
Hello在从编写到运行的过程发生了P2P(From Program to Process),在运行前到运行结束发生了O2O(From Zero-0 to Zero-0)。
Hello的P2P过程按顺序分为两个阶段。第一个阶段从程序源代码到可执行文件,这个阶段按顺序包含预处理、编译、汇编、连接四个过程;第二个阶段从可执行文件到进程,在Linux环境下,这个过程由shell调用fork()函数生成的子进程调用execve()函数实现。
Hello的O2O过程按顺序分为三个阶段。第一个阶段为进程开启阶段,与P2P过程的第二阶段重叠, shell调用fork()函数生成的子进程并映射虚拟内存,而后该子进程调用execve()函数进入Hello的程序入口,并开始载入物理内存;第二阶段为程序执行阶段,该阶段Hello在CPU为其分配的时间片中执行自己的代码,亦可利用信号与其他进程互相通信;第三阶段为进程结束阶段,若此时其父进程shell未结束,则由shell将之回收,否则由init进程将之回收,回收时与进程相关的数据结构将被删除。
1.2 环境与工具
X64 CPU;2.60GHz;16G RAM;1.81THD Disk;
Windows10 64位;VMware 14;Ubuntu 18.04 LTS 64位;gcc;
1.3 中间结果
文件名 用途
hello hello.o链接生成的可执行文件
hello.c 提供的Hello源代码
hello.i hello.c的预处理结果文件
hello.o hello.s的汇编结果文件
hello.o.objdump hello.o的反汇编结果文件
hello.o.readelf hello.o的elf信息
hello.objdump hello的反汇编结果文件
hello.o.readelf hello的elf信息
hello.s hello.i的编译结果文件
pstree.txt hello被挂起后pstree指令的输出结果
表1 中间结果文件表
1.4 本章小结
本章做了本论文和Hello执行过程的概述。罗列了本论文针对的软硬件环境情况以供复现参考。罗列了中间结果文件及其作用以供查询。

第2章 预处理
2.1 预处理的概念与作用
预处理是程序源代码生成可执行文件的过程中,在编译工作前执行执行的过程。就C语言而言,其作用是支持一些语言特性,例如文件包含、宏定义、条件编译、行控制等。就C语言而言,其结果一般为扩展名为i的文件。
在Hello.c文件中,第6、7、8行为预处理指令,其功能分别为为包含stdio.h头文件、包含unistd.h头文件、包含stdlib.h头文件。
另外地,对于有些特殊的编辑环境,预处理指令格式的代码可以指示一些特殊功能,例如VisualStudio提供了#region折叠代码指令
2.2在Ubuntu下预处理的命令
cpp预处理器的预处理命令为cpp [文件名],若需将hello.c进行预处理,并将结果导出到hello.i,则完整命令为cpp hello.c > hello.i。预处理结果如图1。

图1 hello.c预处理结果截图
2.3 Hello的预处理结果解析
Hello的预处理结果为hello.i,其内容共3119行。文件分为若干个段落,每个段落的头部包含一个格式为# %d %s [%d] [%d] [%d] [%d](格式表示使用C语言stdio.h格式控制串风格,方括号表示可选项)的行,其中前部整形数表示原始行号,中部字符串表示内容来源,后部的可选整型数表示标志。标志意义如表2:
标志 意义
1 新文件开始
2 返回到文件
3 来自系统头文件
4 extern
表2 预处理文件标志表
hello.c文件中的所有注释已被清除,其在头部包含的三个头文件即这些头文件中包含的头文件已经按序展开。hello.c的全局变量sleepsecs和主函数main()按序被保留在与处理文件的最后,可以看到从第3100行开始为hello.c的代码。如图2。

图2 hello.i截图
2.4 本章小结
本章介绍了预处理的定义、作用和结果文件格式,并展示了预处理操作在hello.c文件的执行结果。预处理是支持语言特性的重要手段, C语言中预处理让库文件机制,宏机制等多种便捷操作得以实现,为程序员提供了更好的编程体验。

第3章 编译
3.1 编译的概念与作用
编译是程序源代码生成可执行文件的过程中,将高级语言代码翻译为汇编语言代码的过程。并非所有语言都有编译过程,有的语言使用解释机制将高级语言代码翻译为汇编代码,也存在既编译又解释的语言。
其作用主要为支持高级语言的可移植性,完成不依赖硬件的高级语言到依赖硬件的汇编语言的转化,使得同一份高级语言代码可以在不同的机器上使用。就其与解释机制的区别而言,编译使得高级语言到机器语言的转化发生在运行前,有加速程序运行时效率的作用。就程序可读性而言,编译有将高可读性的高级语言转化为低可读性的机器语言,有保护源文件知识产权和算法安全的作用。
3.2 在Ubuntu下编译的命令
gcc编译器的编译指令为gcc -S <文件名>,若需将hello.i编译,完整的指令为gcc -S hello.i。编译结果如图3(已添加要求的编译选项)。

图3 hello.c编译结果截图
3.3 Hello的编译结果解析
Hello的预处理结果为hello.i,其内容共66行。其内容格式为ARM汇编,内容以行为单位排布,分为汇编指令、汇编代码和标号三种。汇编指令指示汇编器在处理时应有的行为,如设定段落、类型等信息或作为跳转标签;汇编代码为程序逻辑的描述,指示程序的行为,将在汇编时翻译为机器代码。标号源程序使用的符号在汇编文件中的继承。
接下来将以Hello为例,解析编译器处理C语言中的各个数据类型和各类操作的方法。
3.3.1数据
3.3.1.1局部变量
Hello涉及到的局部变量共3个,为int型未初始化变量i、为int型传入参数argc和为char*型传入参数argv。
观察hello.s 35到55行,该部分对应于hello.c 21到25行,汇编代码如下:

  1. .L2:
  2.  movl    $0, -4(%rbp)  
    
  3.  jmp .L3  
    
  4. .L4:
  5.  movq    -32(%rbp), %rax  
    
  6.  addq    $16, %rax  
    
  7.  movq    (%rax), %rdx  
    
  8.  movq    -32(%rbp), %rax  
    
  9.  addq    $8, %rax  
    
  10. movq    (%rax), %rax  
    
  11. movq    %rax, %rsi  
    
  12. movl    $.LC1, %edi  
    
  13. movl    $0, %eax  
    
  14. call    printf  
    
  15. movl    sleepsecs(%rip), %eax  
    
  16. movl    %eax, %edi  
    
  17. call    sleep  
    
  18. addl    $1, -4(%rbp)  
    
  19. .L3:
  20. cmpl    $9, -4(%rbp)  
    
  21. jle .L4  
    

可以看到循环控制变量i在汇编代码中为-4(%rbp)。
观察hello.s 27到29行,该部分对应于hello.c 16行,汇编代码如下:

  1. movl %edi, -20(%rbp)
  2. movq %rsi, -32(%rbp)
  3. cmpl $3, -20(%rbp)
    可以看到传入参数argc在汇编代码中为%edx,arvg在汇编代码中为%rsi。
    局部变量通常存储在寄存器或堆栈中,三变量确实分别存储在main函数的堆栈中和寄存器%edx中。
    3.3.1.2全局变量
    Hello涉及到的局部变量共1个,为int型初始化为2.5变量sleepsecs。
    观察hello.s 3到9行,汇编代码如下:
  4. .globl sleepsecs
  5. .data
  6. .align 4
  7. .type sleepsecs, @object
  8. .size sleepsecs, 4
  9. leepsecs:
  10. .long 2
    可以看到全局变量sleepsecs被指定为一个符号,其位置在.data数据节,大小为4,初始化信息为2。全局变量通常存储在数据节中,该变量确实存储在存储在数据节中。
    3.3.1.3常量
    Hello涉及到的局部变量共2个,为字符串型的常量字符串"Usage: Hello \345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201"和为字符串型的常量字符串"Hello %s %s\n"。
    观察hello.s 10到14行,汇编代码如下:
  11. .section .rodata
  12. .LC0:
  13. .string “Usage: Hello \345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201”
  14. .LC1:
  15. .string “Hello %s %s\n”
    可以看到常量字符串位置在.rodata数据节,并且用汇编指令标记了位置。常量通常存储在只读数据节中,两变量确实存储在存储在只读数据节中。
    3.3.1.4数组
    Hello涉及到的数组共1个,为char型的数组argv。
    观察hello.s 39到45行,汇编代码如下:
  16. movq -32(%rbp), %rax
  17. addq $16, %rax
  18. movq (%rax), %rdx
  19. movq -32(%rbp), %rax
  20. addq $8, %rax
  21. movq (%rax), %rax
  22. movq %rax, %rsi
    可以看到数组存储在一片连续空间中,其内容访问方式为取地址后加上偏移量,再解引用。例如对argv[2]的访问即为截取部分前3行,第一行指令将数组首地址取出至%rax,第二行指令将%rax中的地址增加2sizeof(char)=16以求得访问元素的地址,第三行将访问元素的地址解引用完成访问。
    3.3.1.5传入参数
    Hello涉及到的传入参数共2个,为int型的数组argc和为char*型的数组argv。
    观察hello.s 27到28行,汇编代码如下:
  23. movl %edi, -20(%rbp)
  24. movq %rsi, -32(%rbp)
    可以看到传入参数存储在寄存器中。在64位编译模式下,传入参数通常存储在寄存器中,超出数量的部分存储在调用者堆栈中,两传入参数确实存储在寄存器上。
    3.3.2操作
    3.3.2.1赋值
    Hello涉及到的赋值共3次,hello.c第10行的sleepsecs初始化、hello.c第21行的i初始化、hello.c第21行的i自增。
    观察hello.s 9行,汇编代码如下:
  25. .long 2
    可以看到全局变量的初始化赋值直接使用汇编指令进行声明。
    观察hello.s 35到55行,汇编代码如下:
  26. .L2:
  27.  movl    $0, -4(%rbp)  
    
  28.  jmp .L3  
    
  29. .L4:
  30.  movq    -32(%rbp), %rax  
    
  31.  addq    $16, %rax  
    
  32.  movq    (%rax), %rdx  
    
  33.  movq    -32(%rbp), %rax  
    
  34.  addq    $8, %rax  
    
  35. movq    (%rax), %rax  
    
  36. movq    %rax, %rsi  
    
  37. movl    $.LC1, %edi  
    
  38. movl    $0, %eax  
    
  39. call    printf  
    
  40. movl    sleepsecs(%rip), %eax  
    
  41. movl    %eax, %edi  
    
  42. call    sleep  
    
  43. addl    $1, -4(%rbp)  
    
  44. .L3:
  45. cmpl    $9, -4(%rbp)  
    
  46. jle .L4  
    

可以看到局部变量的初始化使用mov指令向变量所在位置写入立即数,自增使用add指令向变量所在位置增加相应的立即数。具体选择的mov指令取决于操作数的类型,在对int型变量i的操作时使用了movl和addl。
3.3.2.2类型转换
Hello涉及到的类型转换共1次,为hello.c第10行的sleepsecs初始化。
观察hello.s 9行,汇编代码如下:

  1. .long 2
    可以看到全局变量初始化时发生的类型转换会直接使用转换结果进行初始化。在这里的转换为浮点型向整型的转换,其遵循的原则为向0舍入,所以2.5的舍入结果为2。
    3.3.2.3算术与逻辑操作
    Hello涉及到的算数操作共1次(不包含栈指针计算和地址计算),为hello.c第21行的i自增,不涉及逻辑操作。
    观察hello.s 52行,汇编代码如下:
  2. addl $1, -4(%rbp)
    可以看到自增运算使用了add指令将-4(%rbp)与1的和存入-4(%rbp)。具体的指令类型和指令取决于计算需求和操作数的类型,在对int型变量i的自增操作时使用了addl。
    3.3.2.4关系操作
    Hello涉及到的关系操作共2次,hello.c第16行的argc比较和hello.c第21行的i比较。
    观察hello.s 第29和第54行,汇编代码如下:
  3. cmpl $3, -20(%rbp)
  4. cmpl $9, -4(%rbp)
    可以看到关系操作使用了cmp指令获取两数的关系,并将结果设置到条件码中。具体的指令取决于操作数的类型,在对int型变量argc和i的自增操作时使用了addl。
    3.3.2.5控制转移
    Hello涉及到的控制转移共2次,hello.c第16行的条件转移和hello.c第21行的i循环转移。
    观察hello.s 第29、30、54和55行,汇编代码如下:
  5. cmpl $3, -20(%rbp)
  6. je .L2
  7. cmpl $9, -4(%rbp)
  8. jle .L4
    可以看到条件跳转使用了cmp指令获取两数的关系,并用j指令进行执行位置跳转。具体使用的指令取决于操作数的类型和跳转条件,在对int型变量argc的不等跳转中使用了cmpl指令和je指令,在对int型变量i的小于跳转中使用了cmpl指令和jle指令,除此之外还可以使用test指令和set指令获取两数关系,使用jmp进行无条件跳转。
    可以看到汇编代码中条件跳转的关系判断与C语言中的条件进入关系正好相反,这是由于编译器希望保持汇编代码的顺序结构与C语言代码相似以减少跳转次数,而C语言的语法为符合条件则向下执行,而汇编语言为符合条件则跳转(不符合条件则向下执行),所以二者逻辑关系相反。在Hello中argc的不等比较变为判断相等,i的小于比较变为大等于比较。
    3.3.2.6函数操作
    Hello涉及到的函数进入和退出操作共1次,为hello.c第12行main函数进入操作,函数调用操作共5次,为hello.c第18行的printf函数调用,第19行的exit函数调用,第23行的printf函数调用,第24行的sleep函数调用和第26行的getchar函数调用。
    观察hello.s 第20行到第26行,汇编代码如下:
  9. .cfi_startproc
  10. pushq %rbp
  11. .cfi_def_cfa_offset 16
  12. .cfi_offset 6, -16
  13. movq %rsp, %rbp
  14. .cfi_def_cfa_register 6
  15. subq $32, %rsp
    可以看到进入函数后,程序将保存调用者帧指针,将原栈指针赋给新帧指针,最后调整栈指针位置。这样的操作保存了调用者的栈指针(保存在新帧指针中)和帧指针以便恢复,并且在调用者的栈帧之上创建了函数自己的栈帧。可以看到在Hello中main函数的栈帧大小为32个字节。
    观察hello.s 第57行,汇编代码如下:
  16. movl $0, %eax
    可以看到退出函数前,程序将函数的返回值保存在寄存器%rax(%eax)中,用于向调用者返回。可以看到Hello中的main函数的返回值为0。
    观察hello.s 第39行到第48行,汇编代码如下:
  17. movq -32(%rbp), %rax
  18. addq $16, %rax
  19. movq (%rax), %rdx
  20. movq -32(%rbp), %rax
  21. addq $8, %rax
  22. movq (%rax), %rax
  23. movq %rax, %rsi
  24. movl $.LC1, %edi
  25. movl $0, %eax
  26. call printf
    可以看到调用printf函数前,程序按参数倒序依次准备函数的各个参数,并将之存放在寄存器中。对于非符点参数,存放顺序(按参数顺序)为:%rdi, %rsi, %rdx, %rcx, %r8, %r9,对于超出部分将存放在调用者栈中,对于符点参数将使用浮点寄存器传递。可以看到printf的第3参数argv[2]被计算并存储到%rdx中,第2参数argv[1]被计算并存储到%rsi中,第1参数.LC1(字符串常量)被存储到%edi中。其他函数调用类似,不再叙述。
    3.4 本章小结
    本章介绍了编译的定义、作用和结果文件格式,展示了预处理操作在hello.i文件的执行结果,并以Hello为例分析了C语言的数据类型和各个操作的编译方式。编译是高级语言向汇编语言转换的过程,是支撑高级语言的语法和可移植性的重要环节。

第4章 汇编
4.1 汇编的概念与作用
汇编是程序源代码生成可执行文件的过程中,将汇编代码翻译为二进制代码的过程。
其作用主要为支持汇编语言的可读性。阅读和编写二进制代码效率低下,难以开展工作,在于机器紧密配合的编程时需要准确的反应机器的运行过程同时具有一定程度的可读性,所以需要有汇编语言作为描述机器运行的语言。汇编即汇编语言转化为机器语言的过程汇编语言的语句与机器语言是一一对应的。
4.2 在Ubuntu下汇编的命令
as汇编器的汇编指令为as [文件名],若需将hello.s编译,完整的指令为as hello.s。编译结果如图4(已添加输出文件命名选项)。

图4 汇编结果截图
4.3 可重定位目标elf格式
一个典型的ELF文件格式如图5。

图5 可重定位ELF文件格式图
使用readelf工具阅读hello.o的elf信息,下文中将使用readelf -a hello.o指令输出的结果作为分析对象。
4.3.1ELF头
ELF头出现在ELF文件的最初部分,包含一些ELF文件的基本信息,如魔数、文件类型、头部大小、节头表位置等。使用readelf工具阅读到的hello.o的ELF头内容如下:

  1. ELF Header:
  2. Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  3. Class: ELF64
  4. Data: 2’s complement, little endian
  5. Version: 1 (current)
  6. OS/ABI: UNIX - System V
  7. ABI Version: 0
  8. Type: REL (Relocatable file)
  9. Machine: Advanced Micro Devices X86-64
  10. Version: 0x1
  11. Entry point address: 0x0
  12. Start of program headers: 0 (bytes into file)
  13. Start of section headers: 1104 (bytes into file)
  14. Flags: 0x0
  15. Size of this header: 64 (bytes)
  16. Size of program headers: 0 (bytes)
  17. Number of program headers: 0
  18. Size of section headers: 64 (bytes)
  19. Number of section headers: 13
  20. Section header string table index: 12
    可以看到ELF文件的各种信息,例如该文件为ELF64可重定位文件,节头表在文件偏移1104字节的位置,ELF头大小为64字节,节头表共13个且大小为64字节等。
    4.3.2节头表
    节头表出现在ELF文件的末尾,包含ELF文件每一节的信息,包括类型、地址、大小和标记等。使用readelf工具阅读到的hello.o的节头表内容如下:
  21. Section Headers:
  22. [Nr] Name Type Address Offset
  23.     Size              EntSize          Flags  Link  Info  Align  
    
  24. [ 0] NULL 0000000000000000 00000000
  25.     0000000000000000  0000000000000000           0     0     0  
    
  26. [ 1] .text PROGBITS 0000000000000000 00000040
  27.     000000000000007d  0000000000000000  AX       0     0     1  
    
  28. [ 2] .rela.text RELA 0000000000000000 00000310
  29.     00000000000000c0  0000000000000018   I      10     1     8  
    
  30. [ 3] .data PROGBITS 0000000000000000 000000c0
  31.    0000000000000004  0000000000000000  WA       0     0     4  
    
  32. [ 4] .bss NOBITS 0000000000000000 000000c4
  33.    0000000000000000  0000000000000000  WA       0     0     1  
    
  34. [ 5] .rodata PROGBITS 0000000000000000 000000c4
  35.    000000000000002b  0000000000000000   A       0     0     1  
    
  36. [ 6] .comment PROGBITS 0000000000000000 000000ef
  37.    000000000000002b  0000000000000001  MS       0     0     1  
    
  38. [ 7] .note.GNU-stack PROGBITS 0000000000000000 0000011a
  39.    0000000000000000  0000000000000000           0     0     1  
    
  40. [ 8] .eh_frame PROGBITS 0000000000000000 00000120
  41.    0000000000000038  0000000000000000   A       0     0     8  
    
  42. [ 9] .rela.eh_frame RELA 0000000000000000 000003d0
  43.    0000000000000018  0000000000000018   I      10     8     8  
    
  44. [10] .symtab SYMTAB 0000000000000000 00000158
  45.    0000000000000180  0000000000000018          11     9     8  
    
  46. [11] .strtab STRTAB 0000000000000000 000002d8
  47.    0000000000000037  0000000000000000           0     0     1  
    
  48. [12] .shstrtab STRTAB 0000000000000000 000003e8
  49.    0000000000000061  0000000000000000           0     0     1  
    
  50. Key to Flags:
  51. W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  52. L (link order), O (extra OS processing required), G (group), T (TLS),
  53. C (compressed), x (unknown), o (OS specific), E (exclude),
  54. l (large), p (processor specific)
    可以看到13个节头表存储的具体信息,例如.text代码段的大小为0x7d字节,位置在0x40等。
    4.3.3符号表
    符号表是.symtab节存储的内容,包含ELF文件的符号信息,为连接器提供符号重定位时所需的信息,包含符号位置、符号类型等。使用readelf工具阅读到的hello.o的符号表内容如下:
  55. Symbol table ‘.symtab’ contains 16 entries:
  56. Num:    Value          Size Type    Bind   Vis      Ndx Name  
    
  57.   0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND   
    
  58.   1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c  
    
  59.   2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1   
    
  60.   3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3   
    
  61.   4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4   
    
  62.   5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5   
    
  63.   6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7   
    
  64.  7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8   
    
  65.  8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6   
    
  66.  9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 sleepsecs  
    
  67. 10: 0000000000000000   125 FUNC    GLOBAL DEFAULT    1 main  
    
  68. 11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts  
    
  69. 12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND exit  
    
  70. 13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf  
    
  71. 14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND sleep  
    
  72. 15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND getchar  
    

可以看到符号表中存放着16条符号信息,例如sleepsecs符号的编号为3,是大小为4字节的全局OBJECT型符号。
4.3.4重定位项目
重定位项目信息是各个.real节中存储的内容,包含符号的重定位信息,例如符号所在段、偏移量、类型和编号等。使用readelf工具阅读到的hello.o的重定位项目信息如下:

  1. Relocation section ‘.rela.text’ at offset 0x310 contains 8 entries:
  2. Offset Info Type Sym. Value Sym. Name + Addend
  3. 000000000016 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
  4. 00000000001b 000b00000002 R_X86_64_PC32 0000000000000000 puts - 4
  5. 000000000025 000c00000002 R_X86_64_PC32 0000000000000000 exit - 4
  6. 00000000004c 00050000000a R_X86_64_32 0000000000000000 .rodata + 1e
  7. 000000000056 000d00000002 R_X86_64_PC32 0000000000000000 printf - 4
  8. 00000000005c 000900000002 R_X86_64_PC32 0000000000000000 sleepsecs - 4
  9. 000000000063 000e00000002 R_X86_64_PC32 0000000000000000 sleep - 4
  10. 000000000072 000f00000002 R_X86_64_PC32 0000000000000000 getchar - 4
  11. Relocation section ‘.rela.eh_frame’ at offset 0x3d0 contains 1 entry:
  12. Offset Info Type Sym. Value Sym. Name + Addend
  13. 000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
    可以看到.text段共有8条重定位信息,eh_frame段共有1条重定位信息,连接器将在连接时对根据重定位项目信息将对应位置重新赋值。例如对于第一条重定位信息,其位置为.text段偏移量为0x16处,其符号为5号,其类型为32位重定位绝对引用,重定位位置为.rodata+0x0,则在链接时.text段本文件部分偏移量为0x16位置的32个位会被修改为.rodata段本文件部分地址+0。其他重定位项目与之类似,不再叙述。
    4.4 hello.o的结果解析
    hello.o为可重定位ELF文件,其中包含二进制信息,并不能直接阅读和执行。使用objdump工具可以将机器代码反汇编,下文将使用objdump -d -r hello.o指令输出的结果作为分析对象,其结果如下:
  14. hello.o: file format elf64-x86-64
  15. Disassembly of section .text:
  16. 0000000000000000 :
  17. 0:   55                      push   %rbp  
    
  18. 1:   48 89 e5                mov    %rsp,%rbp  
    
  19. 4:   48 83 ec 20             sub    $0x20,%rsp  
    
  20. 8: 89 7d ec mov %edi,-0x14(%rbp)
  21. b: 48 89 75 e0 mov %rsi,-0x20(%rbp)
  22. f: 83 7d ec 03 cmpl $0x3,-0x14(%rbp)
  23. 13: 74 14 je 29 <main+0x29>
  24. 15: bf 00 00 00 00 mov $0x0,%edi
  25.         16: R_X86_64_32 .rodata  
    
  26. 1a: e8 00 00 00 00 callq 1f <main+0x1f>
  27.         1b: R_X86_64_PC32   puts-0x4  
    
  28. 1f: bf 01 00 00 00 mov $0x1,%edi
  29. 24: e8 00 00 00 00 callq 29 <main+0x29>
  30.         25: R_X86_64_PC32   exit-0x4  
    
  31. 29: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
  32. 30: eb 39 jmp 6b <main+0x6b>
  33. 32: 48 8b 45 e0 mov -0x20(%rbp),%rax
  34. 36: 48 83 c0 10 add $0x10,%rax
  35. 3a: 48 8b 10 mov (%rax),%rdx
  36. 3d: 48 8b 45 e0 mov -0x20(%rbp),%rax
  37. 41: 48 83 c0 08 add $0x8,%rax
  38. 45: 48 8b 00 mov (%rax),%rax
  39. 48: 48 89 c6 mov %rax,%rsi
  40. 4b: bf 00 00 00 00 mov $0x0,%edi
  41.         4c: R_X86_64_32 .rodata+0x1e  
    
  42. 50: b8 00 00 00 00 mov $0x0,%eax
  43. 55: e8 00 00 00 00 callq 5a <main+0x5a>
  44.         56: R_X86_64_PC32   printf-0x4  
    
  45. 5a: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 60 <main+0x60>
  46.         5c: R_X86_64_PC32   sleepsecs-0x4  
    
  47. 60: 89 c7 mov %eax,%edi
  48. 62: e8 00 00 00 00 callq 67 <main+0x67>
  49.         63: R_X86_64_PC32   sleep-0x4  
    
  50. 67: 83 45 fc 01 addl $0x1,-0x4(%rbp)
  51. 6b: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
  52. 6f: 7e c1 jle 32 <main+0x32>
  53. 71: e8 00 00 00 00 callq 76 <main+0x76>
  54.         72: R_X86_64_PC32   getchar-0x4  
    
  55. 76: b8 00 00 00 00 mov $0x0,%eax
  56. 7b: c9 leaveq
  57. 7c: c3 retq
    可以看到机器语言反汇编的结果与汇编代码hello.s基本一致,但是其中并没有汇编代码中的汇编指令和标号。其中用于表示符号的标号进入了ELF文件的符号表,用于表示其他汇编信息的汇编指令已经被汇编器解析并执行,并不在代码段中出现。而用于标记跳转和引用位置的汇编指令被对应的位置已经被修改,相应地,基于引用的部分被汇编器解析并存储在符号表和重定位信息中,等待连接器在重定位时将之再次写入。例如第30行的mov操作数,33行的函数地址;基于偏移量的部分被计算并填入,例如42行的跳转。
    4.5 本章小结
    本章介绍了汇编的定义、作用和结果文件格式,介绍了重定位的计算方法,并以Hello为例比较了其结果再反汇编后的结果与汇编之前的代码的差异。汇编是汇编语言向机器语言转换的过程,是支撑汇编语言可读性的重要环节。

第5章 链接
5.1 链接的概念与作用
连接是程序源代码生成可执行文件的过程中,将二进制代码组合为可执行文件的过程。
其作用主要为支持C语言的独立编译性。对于规模较大的程序,若在每次修改后均需完整编译,则会耗费大量时间,而独立编译性使得不同的代码可以分时编译而后统一链接,这使得每次修改后只需重新编译修改部分。链接机制为这样的操作提供了基础。
5.2 在Ubuntu下链接的命令
ld链接器的链接指令为ld [文件名]…,若需将hello.o编译,完整的指令为ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.。编译结果如图6。

图6 链接结果截图
5.3 可执行目标文件hello的格式
一个典型的可执行ELF文件结构如图7。

图7 可执行ELF文件格式图
可以看到,相比可重定位ELF文件,可执行ELF文件减少了重定位信息节,增加了段头表和.init节。使用readelf工具阅读其信息如下:

  1. ELF Header:
  2. Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  3. Class: ELF64
  4. Data: 2’s complement, little endian
  5. Version: 1 (current)
  6. OS/ABI: UNIX - System V
  7. ABI Version: 0
  8. Type: EXEC (Executable file)
  9. Machine: Advanced Micro Devices X86-64
  10. Version: 0x1
  11. Entry point address: 0x400500
  12. Start of program headers: 64 (bytes into file)
  13. Start of section headers: 5928 (bytes into file)
  14. Flags: 0x0
  15. Size of this header: 64 (bytes)
  16. Size of program headers: 56 (bytes)
  17. Number of program headers: 8
  18. Size of section headers: 64 (bytes)
  19. Number of section headers: 25
  20. Section header string table index: 24
  21. Section Headers:
  22. [Nr] Name Type Address Offset
  23.    Size              EntSize          Flags  Link  Info  Align  
    
  24. [ 0] NULL 0000000000000000 00000000
  25.    0000000000000000  0000000000000000           0     0     0  
    
  26. [ 1] .interp PROGBITS 0000000000400200 00000200
  27.    000000000000001c  0000000000000000   A       0     0     1  
    
  28. [ 2] .note.ABI-tag NOTE 000000000040021c 0000021c
  29.    0000000000000020  0000000000000000   A       0     0     4  
    
  30. [ 3] .hash HASH 0000000000400240 00000240
  31.    0000000000000034  0000000000000004   A       5     0     8  
    
  32. [ 4] .gnu.hash GNU_HASH 0000000000400278 00000278
  33.    000000000000001c  0000000000000000   A       5     0     8  
    
  34. [ 5] .dynsym DYNSYM 0000000000400298 00000298
  35.    00000000000000c0  0000000000000018   A       6     1     8  
    
  36. [ 6] .dynstr STRTAB 0000000000400358 00000358
  37.    0000000000000057  0000000000000000   A       0     0     1  
    
  38. [ 7] .gnu.version VERSYM 00000000004003b0 000003b0
  39.    0000000000000010  0000000000000002   A       5     0     2  
    
  40. [ 8] .gnu.version_r VERNEED 00000000004003c0 000003c0
  41.    0000000000000020  0000000000000000   A       6     1     8  
    
  42. [ 9] .rela.dyn RELA 00000000004003e0 000003e0
  43.    0000000000000030  0000000000000018   A       5     0     8  
    
  44. [10] .rela.plt RELA 0000000000400410 00000410
  45.    0000000000000078  0000000000000018  AI       5    19     8  
    
  46. [11] .init PROGBITS 0000000000400488 00000488
  47.    0000000000000017  0000000000000000  AX       0     0     4  
    
  48. [12] .plt PROGBITS 00000000004004a0 000004a0
  49.    0000000000000060  0000000000000010  AX       0     0     16  
    
  50. [13] .text PROGBITS 0000000000400500 00000500
  51.    0000000000000122  0000000000000000  AX       0     0     16  
    
  52. [14] .fini PROGBITS 0000000000400624 00000624
  53.    0000000000000009  0000000000000000  AX       0     0     4  
    
  54. [15] .rodata PROGBITS 0000000000400630 00000630
  55.    000000000000002f  0000000000000000   A       0     0     4  
    
  56. [16] .eh_frame PROGBITS 0000000000400660 00000660
  57.    00000000000000fc  0000000000000000   A       0     0     8  
    
  58. [17] .dynamic DYNAMIC 0000000000600e50 00000e50
  59.    00000000000001a0  0000000000000010  WA       6     0     8  
    
  60. [18] .got PROGBITS 0000000000600ff0 00000ff0
  61.    0000000000000010  0000000000000008  WA       0     0     8  
    
  62. [19] .got.plt PROGBITS 0000000000601000 00001000
  63.    0000000000000040  0000000000000008  WA       0     0     8  
    
  64. [20] .data PROGBITS 0000000000601040 00001040
  65.    0000000000000008  0000000000000000  WA       0     0     4  
    
  66. [21] .comment PROGBITS 0000000000000000 00001048
  67.    000000000000002a  0000000000000001  MS       0     0     1  
    
  68. [22] .symtab SYMTAB 0000000000000000 00001078
  69.    0000000000000498  0000000000000018          23    28     8  
    
  70. [23] .strtab STRTAB 0000000000000000 00001510
  71.    0000000000000150  0000000000000000           0     0     1  
    
  72. [24] .shstrtab STRTAB 0000000000000000 00001660
  73.    00000000000000c5  0000000000000000           0     0     1  
    
  74. Key to Flags:
  75. W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  76. L (link order), O (extra OS processing required), G (group), T (TLS),
  77. C (compressed), x (unknown), o (OS specific), E (exclude),
  78. l (large), p (processor specific)
  79. There are no section groups in this file.
  80. Program Headers:
  81. Type Offset VirtAddr PhysAddr
  82.              FileSiz            MemSiz              Flags  Align  
    
  83. PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
  84.              0x00000000000001c0 0x00000000000001c0  R      0x8  
    
  85. INTERP 0x0000000000000200 0x0000000000400200 0x0000000000400200
  86.              0x000000000000001c 0x000000000000001c  R      0x1  
    
  87.   [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]  
    
  88. LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
  89.              0x000000000000075c 0x000000000000075c  R E    0x200000  
    
  90. LOAD 0x0000000000000e50 0x0000000000600e50 0x0000000000600e50
  91.              0x00000000000001f8 0x00000000000001f8  RW     0x200000  
    
  92. DYNAMIC 0x0000000000000e50 0x0000000000600e50 0x0000000000600e50
  93.              0x00000000000001a0 0x00000000000001a0  RW     0x8  
    
  94. NOTE 0x000000000000021c 0x000000000040021c 0x000000000040021c
  95.              0x0000000000000020 0x0000000000000020  R      0x4  
    
  96. GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
  97.                 0x0000000000000000 0x0000000000000000  RW     0x10  
    
  98.  GNU_RELRO      0x0000000000000e50 0x0000000000600e50 0x0000000000600e50  
    
  99.                 0x00000000000001b0 0x00000000000001b0  R      0x1  
    
  100. Section to Segment mapping:  
    
  101.  Segment Sections...  
    
  102.   00       
    
  103.   01     .interp   
    
  104.   02     .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame   
    
  105.   03     .dynamic .got .got.plt .data   
    
  106.   04     .dynamic   
    
  107.   05     .note.ABI-tag   
    
  108.   06       
    
  109.   07     .dynamic .got   
    
  110. Dynamic section at offset 0xe50 contains 21 entries:
  111.  Tag        Type                         Name/Value  
    
  112. 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]  
    
  113. 0x000000000000000c (INIT)               0x400488  
    
  114. 0x000000000000000d (FINI)               0x400624  
    
  115. 0x0000000000000004 (HASH)               0x400240  
    
  116. 0x000000006ffffef5 (GNU_HASH)           0x400278  
    
  117. 0x0000000000000005 (STRTAB)             0x400358  
    
  118. 0x0000000000000006 (SYMTAB)             0x400298  
    
  119. 0x000000000000000a (STRSZ)              87 (bytes)  
    
  120. 0x000000000000000b (SYMENT)             24 (bytes)  
    
  121. 0x0000000000000015 (DEBUG)              0x0  
    
  122. 0x0000000000000003 (PLTGOT)             0x601000  
    
  123. 0x0000000000000002 (PLTRELSZ)           120 (bytes)  
    
  124. 0x0000000000000014 (PLTREL)             RELA  
    
  125. 0x0000000000000017 (JMPREL)             0x400410  
    
  126. 0x0000000000000007 (RELA)               0x4003e0  
    
  127. 0x0000000000000008 (RELASZ)             48 (bytes)  
    
  128. 0x0000000000000009 (RELAENT)            24 (bytes)  
    
  129. 0x000000006ffffffe (VERNEED)            0x4003c0  
    
  130. 0x000000006fffffff (VERNEEDNUM)         1  
    
  131. 0x000000006ffffff0 (VERSYM)             0x4003b0  
    
  132. 0x0000000000000000 (NULL)               0x0  
    
  133. Relocation section ‘.rela.dyn’ at offset 0x3e0 contains 2 entries:
  134.  Offset          Info           Type           Sym. Value    Sym. Name + Addend  
    
  135. 000000600ff0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
  136. 000000600ff8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 gmon_start + 0
  137. Relocation section ‘.rela.plt’ at offset 0x410 contains 5 entries:
  138.  Offset          Info           Type           Sym. Value    Sym. Name + Addend  
    
  139. 000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
  140. 000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
  141. 000000601028 000400000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0
  142. 000000601030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0
  143. 000000601038 000700000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
  144. The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
  145. Symbol table ‘.dynsym’ contains 8 entries:
  146.   Num:    Value          Size Type    Bind   Vis      Ndx Name  
    
  147.     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND   
    
  148.     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)  
    
  149.     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)  
    
  150.     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)  
    
  151.     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getchar@GLIBC_2.2.5 (2)  
    
  152.     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__  
    
  153.     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)  
    
  154.     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@GLIBC_2.2.5 (2)  
    
  155. Symbol table ‘.symtab’ contains 49 entries:
  156.   Num:    Value          Size Type    Bind   Vis      Ndx Name  
    
  157.     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND   
    
  158.     1: 0000000000400200     0 SECTION LOCAL  DEFAULT    1   
    
  159.     2: 000000000040021c     0 SECTION LOCAL  DEFAULT    2   
    
  160.     3: 0000000000400240     0 SECTION LOCAL  DEFAULT    3   
    
  161.     4: 0000000000400278     0 SECTION LOCAL  DEFAULT    4   
    
  162.     5: 0000000000400298     0 SECTION LOCAL  DEFAULT    5   
    
  163.     6: 0000000000400358     0 SECTION LOCAL  DEFAULT    6   
    
  164.     7: 00000000004003b0     0 SECTION LOCAL  DEFAULT    7   
    
  165.     8: 00000000004003c0     0 SECTION LOCAL  DEFAULT    8   
    
  166.     9: 00000000004003e0     0 SECTION LOCAL  DEFAULT    9   
    
  167.    10: 0000000000400410     0 SECTION LOCAL  DEFAULT   10   
    
  168.    11: 0000000000400488     0 SECTION LOCAL  DEFAULT   11   
    
  169.    12: 00000000004004a0     0 SECTION LOCAL  DEFAULT   12   
    
  170.    13: 0000000000400500     0 SECTION LOCAL  DEFAULT   13   
    
  171.    14: 0000000000400624     0 SECTION LOCAL  DEFAULT   14   
    
  172.    15: 0000000000400630     0 SECTION LOCAL  DEFAULT   15   
    
  173.    16: 0000000000400660     0 SECTION LOCAL  DEFAULT   16   
    
  174.    17: 0000000000600e50     0 SECTION LOCAL  DEFAULT   17   
    
  175.    18: 0000000000600ff0     0 SECTION LOCAL  DEFAULT   18   
    
  176.    19: 0000000000601000     0 SECTION LOCAL  DEFAULT   19   
    
  177.    20: 0000000000601040     0 SECTION LOCAL  DEFAULT   20   
    
  178.    21: 0000000000000000     0 SECTION LOCAL  DEFAULT   21   
    
  179.    22: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c  
    
  180.    23: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS   
    
  181.    24: 0000000000600e50     0 NOTYPE  LOCAL  DEFAULT   17 __init_array_end  
    
  182.    25: 0000000000600e50     0 OBJECT  LOCAL  DEFAULT   17 _DYNAMIC  
    
  183.    26: 0000000000600e50     0 NOTYPE  LOCAL  DEFAULT   17 __init_array_start  
    
  184.    27: 0000000000601000     0 OBJECT  LOCAL  DEFAULT   19 _GLOBAL_OFFSET_TABLE_  
    
  185.    28: 0000000000400620     2 FUNC    GLOBAL DEFAULT   13 __libc_csu_fini  
    
  186.    29: 0000000000601040     0 NOTYPE  WEAK   DEFAULT   20 data_start  
    
  187.    30: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@@GLIBC_2.2.5  
    
  188.    31: 0000000000601044     4 OBJECT  GLOBAL DEFAULT   20 sleepsecs  
    
  189.    32: 0000000000601048     0 NOTYPE  GLOBAL DEFAULT   20 _edata  
    
  190.    33: 0000000000400624     0 FUNC    GLOBAL DEFAULT   14 _fini  
    
  191.    34: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5  
    
  192.    35: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_  
    
  193.    36: 0000000000601040     0 NOTYPE  GLOBAL DEFAULT   20 __data_start  
    
  194.    37: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getchar@@GLIBC_2.2.5  
    
  195.    38: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__  
    
  196.    39: 0000000000400630     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used  
    
  197.    40: 00000000004005b0   101 FUNC    GLOBAL DEFAULT   13 __libc_csu_init  
    
  198.    41: 0000000000601048     0 NOTYPE  GLOBAL DEFAULT   20 _end  
    
  199.    42: 0000000000400530     2 FUNC    GLOBAL HIDDEN    13 _dl_relocate_static_pie  
    
  200.    43: 0000000000400500    43 FUNC    GLOBAL DEFAULT   13 _start  
    
  201.    44: 0000000000601048     0 NOTYPE  GLOBAL DEFAULT   20 __bss_start  
    
  202.    45: 0000000000400532   125 FUNC    GLOBAL DEFAULT   13 main  
    
  203.    46: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@@GLIBC_2.2.5  
    
  204.    47: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@@GLIBC_2.2.5  
    
  205.    48: 0000000000400488     0 FUNC    GLOBAL DEFAULT   11 _init  
    
  206. Histogram for bucket list length (total of 3 buckets):
  207. Length  Number     % of total  Coverage  
    
  208.      0  0          (  0.0%)  
    
  209.      1  0          (  0.0%)      0.0%  
    
  210.      2  2          ( 66.7%)     57.1%  
    
  211.      3  1          ( 33.3%)    100.0%  
    
  212. Version symbols section ‘.gnu.version’ contains 8 entries:
  213. Addr: 00000000004003b0  Offset: 0x0003b0  Link: 5 (.dynsym)  
    
  214.  000:   0 (*local*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)  
    
  215.  004:   2 (GLIBC_2.2.5)   0 (*local*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)  
    
  216. Version needs section ‘.gnu.version_r’ contains 1 entry:
  217. Addr: 0x00000000004003c0  Offset: 0x0003c0  Link: 6 (.dynstr)  
    
  218.  000000: Version: 1  File: libc.so.6  Cnt: 1  
    
  219.  0x0010:   Name: GLIBC_2.2.5  Flags: none  Version: 2  
    
  220. Displaying notes found in: .note.ABI-tag
  221.  Owner                 Data size   Description  
    
  222.  GNU                  0x00000010   NT_GNU_ABI_TAG (ABI version tag)  
    
  223.    OS: Linux, ABI: 3.2.0  
    

其中的节头表(第22行到底79行)存储了各段的基本信息,例如第13条.text的起始地址为0x400500,偏移量为0x500,大小为0x122等。与可重定位ELF文件类似,不再叙述。
5.4 hello的虚拟地址空间
使用edb加载hello,可以从DataDump中看到内存中从0x400000起始,每个节依次罗列,其位置如节头表中的地址所标记,例如.text从0x400500开始。如图8从反汇编窗口中明显的看到,400500位置前的反汇编明显是混乱的。

图8 hello的.text段内存情况
亦可以看到内存中从0x600e50起始,动态链接的部分依次罗列,这与程序头部表(第83行到第102行)标记的信息是一致的,如图9。

图9 hello的动态链接内存情况
5.5 链接的重定位过程分析
使用objdump将hello进行反汇编,其结果如下(节选.main和.init):

  1. 0000000000400532 :
  2. 400532: 55 push %rbp
  3. 400533: 48 89 e5 mov %rsp,%rbp
  4. 400536: 48 83 ec 20 sub $0x20,%rsp
  5. 40053a: 89 7d ec mov %edi,-0x14(%rbp)
  6. 40053d: 48 89 75 e0 mov %rsi,-0x20(%rbp)
  7. 400541: 83 7d ec 03 cmpl $0x3,-0x14(%rbp)
  8. 400545: 74 14 je 40055b <main+0x29>
  9. 400547: bf 34 06 40 00 mov $0x400634,%edi
  10. 40054c: e8 5f ff ff ff callq 4004b0 puts@plt
  11. 400551: bf 01 00 00 00 mov $0x1,%edi
  12. 400556: e8 85 ff ff ff callq 4004e0 exit@plt
  13. 40055b: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
  14. 400562: eb 39 jmp 40059d <main+0x6b>
  15. 400564: 48 8b 45 e0 mov -0x20(%rbp),%rax
  16. 400568: 48 83 c0 10 add $0x10,%rax
  17. 40056c: 48 8b 10 mov (%rax),%rdx
  18. 40056f: 48 8b 45 e0 mov -0x20(%rbp),%rax
  19. 400573: 48 83 c0 08 add $0x8,%rax
  20. 400577: 48 8b 00 mov (%rax),%rax
  21. 40057a: 48 89 c6 mov %rax,%rsi
  22. 40057d: bf 52 06 40 00 mov $0x400652,%edi
  23. 400582: b8 00 00 00 00 mov $0x0,%eax
  24. 400587: e8 34 ff ff ff callq 4004c0 printf@plt
  25. 40058c: 8b 05 b2 0a 20 00 mov 0x200ab2(%rip),%eax # 601044
  26. 400592: 89 c7 mov %eax,%edi
  27. 400594: e8 57 ff ff ff callq 4004f0 sleep@plt
  28. 400599: 83 45 fc 01 addl $0x1,-0x4(%rbp)
  29. 40059d: 83 7d fc 09 cmpl $0x9,-0x4(%rbp)
  30. 4005a1: 7e c1 jle 400564 <main+0x32>
  31. 4005a3: e8 28 ff ff ff callq 4004d0 getchar@plt
  32. 4005a8: b8 00 00 00 00 mov $0x0,%eax
  33. 4005ad: c9 leaveq
  34. 4005ae: c3 retq
  35. 4005af: 90 nop
  36. 00000000004005b0 <__libc_csu_init>:
  37. 4005b0: 41 57 push %r15
  38. 4005b2: 41 56 push %r14
  39. 4005b4: 49 89 d7 mov %rdx,%r15
  40. 4005b7: 41 55 push %r13
  41. 4005b9: 41 54 push %r12
  42. 4005bb: 4c 8d 25 8e 08 20 00 lea 0x20088e(%rip),%r12 # 600e50 <_DYNAMIC>
  43. 4005c2: 55 push %rbp
  44. 4005c3: 48 8d 2d 86 08 20 00 lea 0x200886(%rip),%rbp # 600e50 <_DYNAMIC>
  45. 4005ca: 53 push %rbx
  46. 4005cb: 41 89 fd mov %edi,%r13d
  47. 4005ce: 49 89 f6 mov %rsi,%r14
  48. 4005d1: 4c 29 e5 sub %r12,%rbp
  49. 4005d4: 48 83 ec 08 sub $0x8,%rsp
  50. 4005d8: 48 c1 fd 03 sar $0x3,%rbp
  51. 4005dc: e8 a7 fe ff ff callq 400488 <_init>
  52. 4005e1: 48 85 ed test %rbp,%rbp
  53. 4005e4: 74 20 je 400606 <__libc_csu_init+0x56>
  54. 4005e6: 31 db xor %ebx,%ebx
  55. 4005e8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
  56. 4005ef: 00
  57. 4005f0: 4c 89 fa mov %r15,%rdx
  58. 4005f3: 4c 89 f6 mov %r14,%rsi
  59. 4005f6: 44 89 ef mov %r13d,%edi
  60. 4005f9: 41 ff 14 dc callq *(%r12,%rbx,8)
  61. 4005fd: 48 83 c3 01 add $0x1,%rbx
  62. 400601: 48 39 dd cmp %rbx,%rbp
  63. 400604: 75 ea jne 4005f0 <__libc_csu_init+0x40>
  64. 400606: 48 83 c4 08 add $0x8,%rsp
  65. 40060a: 5b pop %rbx
  66. 40060b: 5d pop %rbp
  67. 40060c: 41 5c pop %r12
  68. 40060e: 41 5d pop %r13
  69. 400610: 41 5e pop %r14
  70. 400612: 41 5f pop %r15
  71. 400614: c3 retq
  72. 400615: 90 nop
  73. 400616: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
  74. 40061d: 00 00 00
    可以看到4.4节分析的待重定位项已经被重定位完成,例如22行的mov参数和24行的函数地址。以22行的mov参数分析重定位过程,该参数被定位为.rodata+0x1e处,查看两文件.rodata段的大小,发现链接前.rodata段的大小为0x2b,连接后.rodata段的大小为0x2f,所以有0x4大小的内容被并入.rodata段。根据段头表.rodata将被加载到0x400630处,所以该参数的最终为0x40064e或0x400652,根据反汇编信息,该参数确实为0x400652。
    5.6 hello的执行流程
  75. call ld_start
  76. call ld_init
  77. jmp _start
  78. call __libc_start_main
  79.  call __cxa_atexit  
    
  80.      call __new_exitfn  
    
  81.  call __libc_csu_init  
    
  82.      call _init  
    
  83.      call 0x56021492c860  
    
  84.         jmp register_tm_clones  
    
  85. call _setjmp  
    
  86.         jmp __sigsetjmp  
    
  87.             jmp __sigjmp_save  
    
  88. call main  
    
  89. call exit  
    
  90.     call __run_exit_handlers  
    
  91.         call __call_tls_dtors  
    
  92.         call _dl_fini  
    
  93.             call rtld_lock_default_lock_recursive  
    
  94.             call _dl_sort_maps  
    
  95.                 call memset  
    
  96.             call rtld_lock_default_unlock_recursive  
    
  97.             call 0x563ad6ed4820  
    
  98.                 call 0x563ad6ed4758  
    
  99.                     jmp __cxa_finalize  
    
  100.                         call __unregister_atfork  
    
  101.                 call deregister_tm_clones  
    
  102.             call 0x563ad6ed4ef4  
    
  103.         call _IO_cleanup  
    
  104.             call _IO_flush_all_lockp  
    
  105.             call __GI__IO_file_setbuf  
    
  106.                 call _IO_default_setbuf  
    
  107.                     call __GI__IO_file_sync  
    
  108.         call _exit  
    
  109.             syscall exit_group()  
    

5.7 Hello的动态链接分析
在动态链接前后,动态链接的全局偏移量表发生变化,其变化前后如图10和图11。

图10 变化前图

图11 变化后图
可以看到0x600ff0即之后位置发生了变化。这是动态链接获取共享库函数地址的结果。查看此地址内容,可以看到动态链接的函数的代码如图9。这些函数在第一次访问时也会先至该表出查到动态地址,并根据重定位信息表查到待修改的信息后直接修改调用地址,已实现后续调用的一次访问。
5.8 本章小结
本章介绍了链接的定义、作用和结果文件格式,介绍了重定位的计算方法,以Hello为例比较了其结果再反汇编后的连接前文件反汇编的差异,并分析了Hello的执行过程和动态链接过程。链接时将不同的二进制代码组合的过程,是语言独立编译性的重要环节。

第6章 hello进程管理
6.1 进程的概念与作用
进程是程序执行的一个实例,是占有计算和存储资源的基本单位,由内核负责维护。进程的作用是为程序提供了独占CPU和内存资源的抽象,使得编写程序时无需考虑同时运行的其他程序。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型应用级程序,代表用户运行其他程序,并作为父进程负责运行这些程序的子进程的创建、回收和转发信号。shell的功能为读取、解析并执行用户的命令,这些命令可能是shell内置命令,也可能是代表用户运行其他程序。shell的处理流程为无限循环读取命令和求值(解析并执行命令)的过程,直到用户使用shell提供的方法将其退出。求值过程中shell将拆分用户输入的命令行,判断用户输入的是否为内置命令,如果用户输入内置命令,则调用相应的函数予以执行,否则将创建子进程执行用户指定的程序。
6.3 Hello的fork进程创建过程
在Shell中输入Hello的地址作为命令,Shell将分析并响应输入命令。Shell首先判断该命令是否为内置命令,其结果为否;而后Shell将认为这条命令是一个程序地址,所以调用fork函数进行子进程创建,该子进程将会是运行Hello的进程。此时该进程具有与Shell相同的地址空间副本,并且开始占用CPU时间片,与父进程Shell并发地运行。
6.4 Hello的execve过程
Shell调用fork()生成的子进程会使用命令为参数调用execve()函数,若该命令确为Hello的地址,则execve()函数会调用加载器将虚拟内存中的现有内容删除并重新创建和初始化新的段,将各段映射到可执行文件或映射为片,设置PC指向程Hello程序的入口处,即完成Hello的execve过程。为了提高运行速度,此时并未有内容被加载,而是随CPU的引用逐渐加载,这取决于虚拟内存管理策略。
6.5 Hello的进程执行
Hello进程将在内核为之分配的时间片中执行其逻辑流,当时间片结束时,将转为核心态,转由内核工作。内核将使用上下文切换执行进程调度,内核将保存Hello的上下文以便恢复其逻辑流,而后恢复其调度的进程的上下文,最后将控制转交给其调度的进程,重新转换为用户态。待一定时间后,内存将再次调度Hello进程,发生上下文切换,Hello将继续执行直至时间片结束。
6.6 hello的异常与信号处理
在Hello的执行中可能会出现故障异常,例如在读取时发生缺页则会出现缺页异常,将会由相关加载器进行页的加载,加载完成后会恢复至当前语句。Hello在运行中可能产生SIGCHLD信号,例如在停止或终止时,其产生的信号将被父进程接受,对于其父进程Shell而言,一般会根据情况对其进行回收。Hello在运行周可能接受任何信号,使用其默认的信号处理方法进行处理,例如接收到SIGTSTP信号则会挂起。
在运行中乱按键盘情况如图12。其中第一种情况为在乱按中不按回车,直到程序运行至getchar()再按回车,程序正常。第二种情况为在乱按中按回车,程序的getchar()将响应乱按中的回车,而后续的字符将被视为下一条命令。这说明了在选中Shell的情况下乱按除了输出字符外还会将字符缓存到标准输入流中。

图12 运行中乱按情况截图
在运行中按回车情况如图13。程序的getchar()不会再行等待,更加印证了乱按的结论。

图13 运行中按回车情况
在运行中按Ctrl-C情况如图14。进程接收到由Shell向前台进程组发送的SIGINT信号,直接结束。

图14 运行中按Ctrl-C情况
在运行中按Ctrl-Z情况如图15。可以看到进程被挂起,使用ps指令查看进程可以看到该进程存在,使用jobs指令可以看到该该进程被暂停,使用pstree指令可以看到hello为bash的子进程,使用fg指令可以将解除进程暂停并置于前台运行,使用kill指令可以向进程发送信号,在发送9号信号后,该进程停止。

图15 运行中按Ctrl-Z情况
6.7本章小结
本章介绍了进程的概念和作用,Shell的原理和Hello的fork、execve、运行和信号与异常处理。进程是操作系统管理运行中程序的基本单位,在进程机制的管理下,程序可以无需考虑其他程序而独立的运行着,操作系统也可以正确的处理多个程序的同时运行。

第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址是物理内存的地址,也是CPU数据总线上传输的地址。该地址被物理内存获取并在内存中寻找到相应的数据以供使用。逻辑地址是程序机器代码执行时使用的地址,该地址分为段选择符和偏移地址两部分。线性地址是段地址转化后的地址,该地址分为描述符和偏移量两部分。虚拟地址即线性地址。
在Hello中一个地址程序要访问的逻辑地址先经历段机制转化为线性地址,而后经历分页机制转化为物理地址,最后使用物理地址访存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址会先被分为段选择符和选内偏移量两部分,而后段选择符被进一步拆分为索引、TI和RPL,而后根据TI的值选择使用GDT或LDT依索引查找段描述符,最后将段描述符作为基地址加上段内偏移量得到线性地址。其中GDT和LDT的地址均在用户不可见的寄存器中。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址将会被分为虚拟页号VPN和虚拟也偏移量VPO两部分,而后以VPN为索引在页表中查找物理页号PPN,若发生页命中则直接将PPN和物理页偏移量PPO组合得到物理地址,其中VPO和PPO是相等的;若发生也不命中则从磁盘上将页面读取到内存中,同时更新页表并恢复执行,而后将PPN和物理页偏移量PPO组合得到物理地址并再次访存,此时由于请求字已经调入内存,所以会发生命中。
7.4 TLB与四级页表支持下的VA到PA的变换
在四级页表下,虚拟地址会被拆分成VPN1、VPN2、VPN3、VPN4和VPO,而后使用VPN1为索引查找一级页表,得到二级页表地址,而后以VPN2位索引搜索目标二级页表,得到三级页表地址,而后以VPN3位索引查找目标三级页表,得到四级页表地址,而后以VPN4位索引查找目标四级页表,得到PPN,将之与PPO组合,得到物理地址。若发生不命中则与单页表相似,但是会逐级更新页表。由于页表更新代价大,所以使用较为复杂的算法进行淘汰。
7.5 三级Cache支持下的物理内存访问
物理地址会根据Cache类型被拆分为标记、组索引和块偏移或者标记和块偏移。首先根据组索引(如果有)找到对应的Cache组,而后同时与组内的所有行进行标记的比较,如果存在标记相同且有效的行则发生命中,而后根据块偏移读取数据;否则发生不命中,而后会向下一级Cache直至内存查询并调入数据,同时更新Cache行。相比页表更新,淘汰时使用较简易的算法,例如最近最少使用算法。
7.6 hello进程fork时的内存映射
在新进程创建时,两个进程拥有独立的虚拟内存副本,但是此时两个副本中的对应位置还指向统一内存,当任意一个进程修改了虚拟内存时,才将相应的虚拟内存进行写时复制。目的是提高运行效率,避免不必要的复制。
7.7 hello进程execve时的内存映射
首先会删除已存在的用户区域的结构,并且为Hello创建新的区域结构。而后将hello的代码和数据区映射到hello文件的.text节和.data节,同时将堆栈去置零,bss区置零并映射到匿名文件中。而后将共享区与动态链接到程序中的libc.so映射。
7.8 缺页故障与缺页中断处理
当引用一个不存在没有加载到物理内存中的虚拟地址时,系统就必须从磁盘中取出相应的页更新到内存中。在等待访问硬盘的时间中程序无法继续执行,所以需要引发缺页故障以中断程序。缺页故障的处理程序会等待页面读取完成时使得返回到发生故障的当前语句继续执行,此时应当能发生页命中,程序将继续向下执行。
7.9动态存储分配管理
堆可能会被多个部分申请使用,为了保证同一内存不被不同的位置同时使用,所以要进行动态存储的分配管理。分配器将堆划分为大小不同的块进行管理,堆的内存区域是连续的,每个块是连续的也是互相相邻的。
堆的管理可以可以采用隐式的或显式的方法。隐式的方法不保存指向下一个堆的地址,而是通过获取堆的大小并与当前堆的地址相加得到下一个堆的地址。这样的方法并不能区分下一个搜索的目标堆是否已经被分配,所以效率较低。显式的方法保存指向下一个堆的地址,需要被搜索的是空闲的块,所以显式的方法在空闲块中存入下一个空闲块的地址,通过读取地址直接所引导下一个空闲块,进而进行适配搜索。显式的方法可以使用多种结构,例如显示空闲链表和分离的显示空闲链表,前者将空闲块直接相连,搜索速度较慢,后者将空闲块按大小分类并分别相连,搜索速度较快。在平均的情况下使用红黑树等复杂度为O(logn)的查找结构是最优的。
为了提高空间利用率,分配器需要保证堆每个空闲块尽可能大,所以不应存在相邻的空闲块,所以需要定期将空闲块合并。为了保证能够向前合并,必须在空闲块的尾部存储其大小信息,使得后继块能正确的找到前驱块的地址完成合并。合并可以在每次释放后进行,也可以再每次申请前集中进行,后者的效率更高。
7.10本章小结
本章介绍了Hello的地址空间、虚拟地址和物理地址的转换、高速缓存的原理、Hello不同阶段的内存映射、缺页中断和动态存储分配管理。存储结构的访问是计算机运行时耗时较多的过程,其高效的管理机制对提高机器速度意义重大。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Unix系统将所有的IO设备统一作为文件管理,将IO设备的读写统一为对文件的读写。Unix I/O是Linux内核引出的IO应用接口。
8.2 简述Unix IO接口及其函数
Unix I/O接口可以实现文件的打开、改变位置、读写和关闭。对于每个进程都会创建标准输入、标准输出和标准错误三个打开的文件。UNIX I/O的函数有open(), close(), read(), write(),用于实现文件的打开、关闭、读和写。
8.3 printf的实现分析
首先调用vsprintf函数将格式控制符表示的内容代入入到格式串中,而后调用write系统函数,并调用INT_VECTOR_SYS_CALLA系统调用,该系统调用将输出信息从复制到显存中。而后字符显示驱动子程序从ASCII到字模库查找出字符的点阵信息存储到显示vram中。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点,最终打印出目标字符串。
8.4 getchar的实现分析
当键盘输入时,键盘接口会得到一个键盘的扫描码,同时发出一个中断请求。中断请求会打断当前的形成并执行中断处理子程序,该子程序从键盘接口得到扫描码并将之转换为ASCII码存入键盘缓冲区。
getchar调用read系统函数。read函数读取按键ascii码,直到接受到回车键才返回整个字符串,getchar将其第一个字符取出并返回。
8.5本章小结
本章介绍了Linux的IO管理机制,IO接口、printf的实现和getchar的实现。IO设备是用户与机器交互的桥梁,Linux系统将所有IO统一抽象为文件的方式对非常利于开发者的开发,也利于统一管理。
结论
hello程序经过如下:

  1. 代码编写:使用编辑器输入hello.c的代码。
  2. 预处理:使用预处理器将代码中的预处理指令进行展开和替换。
  3. 编译:使用编译器将C语言代码编译为汇编语言代码。
  4. 汇编:使用汇编器将汇编语言代码汇编为机器代码。
  5. 链接:使用连接器将重定位项进行重定位,将多个机器代码组合成可执行文件。
  6. 创建子进程:shell收到指令后创建子进程准备运行hello。
  7. 启动程序:子进程使用execve函数为hello准备虚拟内存环境
  8. 访存:在运行时随CPU的引用逐渐将所需内容调入内存,并进行访问。
  9. 动态链接:程序与动态库进行链接,获取相应函数的地址。
  10. 执行:在CPU分配的时间片中执行程序,并接受内核的进程管理。
  11. 动态内存申请与释放:在printf中调用malloc函数向分配器申请并获得堆空间。在使用结束后调用free函数向分配器申请释放。
  12. 接受并处理信号:接受其他进程发送的信号,并进行相应的相应。
  13. 中断处理:对于缺页或键盘输入等中断,停止执行逻辑流,进入中断处理,在中断处理完成后返回或结束。
  14. 结束:程序结束,进程终止,由Shell或init进程进行子进程回收,内核删除相关数据结构。
    对计算机系统的设计和实现的感悟:
    计算机是自动化作业的高度集中,是效率的极大体现,也是计算机的核心优势。所以计算机系统的设计始终围绕着时间效率和空间效率两大核心,进而形成了二进制的信息表示、顺序执行与跳转简单组合的程序结构、流水线的处理体系和各种与硬件相适应的优化手段。这些内容的应用为计算机系统提供了极高的综合效率,是指能够承担人力和机械结构无法远无法企及的复杂工程。
    其次,计算机系统的发展面相实用性而展开,其中最重要的部分是设计了成功的存储器层次结构,使得计算机的成本降低,成为计算机大规模实用的基础。在这之上,计算机系统又包含围绕编译与连接展开的程序生成机制,围绕进程展开的运行时管理机制和围绕文件展开的IO机制等,为开发者和用户提供了更好的使用环境。同时也在更高的层次上继续追求时空效率,例如虚拟内存和动态内存管理机制就是提高时空效率的重要环节。
    计算机系统的发展在今后也应当是面相效率和实用性而展开的,有意义的创新也应当发生于此。能否实现更好的流水线,做更远更准确的预测?能否抽取出更多的协处理器?能否发展更多层级的存储器结构和更好的数量配比?能否开发更有效的页面牺牲算法?能否提供更舒适的用户服务?这些均是我理解中计算机系统领域有趣又有意义的讨论方向。

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试

关闭