摘要
本篇文章是2022春季计算机系统课程的大作业,介绍了hello程序的一生,即其从hello.c这个C文件开始经过预处理,编译,汇编,链接等操作最后展现在用户面前,最终进程被杀死,也就是死亡的过程。hello经历了这样的一生,这一个程序可能对于程序员来讲不复杂,但是这个过程中其底层的软硬件的配合过程是很复杂的。本文将这个过程细化为一个个章节,并且逐节讲述。
关键词:计算机系统;编译;链接;
目录
第1章 概述
1.1 Hello简介
P2P:P2P是from program to process的缩写,经过编辑器中的编辑操作得到hello程序,之后经过预处理、编译、汇编、链接生成可执行文件hello,以linux操作系统为例,在linux终端中使用./hello指令执行该文件,shell会解析这段命令,使用fork创建子进程,并且通过execve等函数进行运行这个进程,至此完成program to process的过程
020:shell fork出一些有关于hello的进程,并且映射出虚拟函数,产生属于自己的内存地址、时间周期等。在进程运行结束后释放内存,删除有关上下文。这时hello的进程就像在机器中没有存在过一般(变成了0),也就是实现了0到0的过程。
1.2 环境与工具
硬件:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件:Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04
LTS 64位/优麒麟 64位 以上;
工具:Codeblocks 64位,vim,gcc,gedit,gdb,edb
1.3 中间结果
文件名 | 作用 |
hello.c | 储存C语言源代码 |
hello.i | 预处理生成的文件 |
hello.s | 编译生成的文本文件 |
hello.o | 汇编器生成的二进制文件 |
elf.txt | hello.o文件的可执行可链接文件的txt文件,包含hello.o的信息 |
hello | 链接操作生成的可执行文件 |
hello1.elf | hello文件的可执行可链接文件,包含hello的信息 |
1.4 本章小结
本章主要介绍了hello的p2p,o2o过程,hello的简介,以及实验的环境,硬件、软件、工具。写出了为编写本论文,生成的中间结果文件的名字,文件的作用等。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理器根据以字符#开头的命令,修改原始的C程序,比如hello.c中的第一行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它插入到程序文本中,结果就得到了另一个以.i作为文件扩展名的C程序。
预处理的作用:
1. 将#include、宏定义、预编译指令如#if, #elif等展开或处理插入原文件
2. 删除所有注释
3. 添加行号信息文件名信息,便于调试;
4. 规定某段代码的条件与限制
2.2在Ubuntu下预处理的命令
gcc -E -o hello.i hello.c
预处理过程及结果:
生成了一个.i文件,如下图:
2.3 Hello的预处理结果解析
打开.i文件:发现程序已经被拓展成3060行,原来的hello.c出现在3047行之后。
如下图,可以看出在预处理过程中,程序的头文件、宏定义、扩展库等,hello.i程序的依次开始是程序的头文件stdio.h,unistd.h,stdlib.h的依次展开。例如下图的stdio.h均被展开成很多行的代码(13行和14行明确给出),我们的原来程序(删除了头文件、扩展名等的)在.i文件最末尾处。
2.4 本章小结
本章主要介绍了预处理的概念与作用,以及ubuntu下如何进行预处理操作,并且介绍了预处理的结果解析,了解了预处理的内涵。
第3章 编译
3.1 编译的概念与作用
编译程序就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。 编译器将文本文件 hello.i 翻译成文本文件 hello.s。
作用:通过语法分析,检验是否符合语法标准,如果不符合就会出现错误,将hello.i翻译为hello.s,在编译的同时也会优化程序性能,可以很好地实现其原有机器语言功能的前提下提高编译器程序的性能和运行的效率。从高级语言转换为汇编语言,从而让语言更容易被机器理解。
3.2 在Ubuntu下编译的命令
终端输入以下命令:
gcc -S hello.i -o hello.s
编译过程:
产生.s文件:
3.3 Hello的编译结果解析
3.3.1 常量与赋值
例如程序中的:
- if(argc!=4)
这里面的常量4在汇编语言中这样显示:
- movl %edi, -20(%rbp)
- movq %rsi, -32(%rbp)
- cmpl $4, -20(%rbp)
- je .L2
这里面$4就被存在了内存-20(%rbp)处,
同样的0,8,1,2,3也都是用这种方式存在了寄存器或者内存之中
3.3.2 局部变量与其赋值
局部变量会被存储在寄存器或者栈中,
例如这个初始化,后面i被设置为0
- int i;
他的汇编语言是:
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
说明这个局部变量被存储在内存之中。
3.3.3 数组/指针/结构操作
例如这个指针数组
- char *argv[]
根据main的这段汇编代码:
- main:
- .LFB6:
- .cfi_startproc
- endbr64
- pushq %rbp
- .cfi_def_cfa_offset 16
- .cfi_offset 6, -16
- movq %rsp, %rbp
- .cfi_def_cfa_register 6
- subq $32, %rsp
- movl %edi, -20(%rbp)
- movq %rsi, -32(%rbp)
- cmpl $4, -20(%rbp)
- je .L2
- leaq .LC0(%rip), %rdi
- call puts@PLT
- movl $1, %edi
- call exit@PLT
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
可以看出数组的首地址也就是argv[0]存在了-32(%rbp)的位置中,也是%rsi所存内容。
顺便也可以知道argc存在了-20(%rbp)处
同理也由下面汇编语言可以知道
- movq -32(%rbp), %rax
- addq $24, %rax
- movq (%rax), %rax
- movq %rax, %rdi
- call atoi@PLT
-8(%rbp)地址所存的内容为argv[3]
3.3.4 算术操作
关于i++的汇编语言表示:
使用add操作即可:
- addl $1, -4(%rbp)
3.3.5 关系操作
例如这个判断条件的代码:
- if(argc!=4)
在汇编语言中这样表示:
- cmpl $4, -20(%rbp)
- je .L2
就是使用cmp加上jmp条件跳转进行操作
同样的,循环中的最终条件判断也是类似的:
- cmpl $7, -4(%rbp)
- jle .L4
3.3.6 控制转移
这里面的if和for都使用了控制转移的指令
套路就是cmp比较,加上jmp条件跳转(其实和关系操作的差不太多)
例如这个判断条件的代码:
- if(argc!=4)
在汇编语言中这样表示:
- cmpl $4, -20(%rbp)
- je .L2
也就是判断一个数是否等于4,如果等于就跳转到.L2,否则不跳转。
同样的,循环中的最终条件判断也是类似的:
- cmpl $7, -4(%rbp)
- jle .L4
判断一个数是否小于等于7,如果是就跳转,否则不跳转。
3.3.7 函数操作
函数操作就是在调用之前向各个寄存器中传递参数,顺序为rdi, rsi, rdx, rcx, r8, r9,其余参数放在栈中,然后使用call进行函数的访问,之后将返回值存入rax,最后回到返回位置的操作。
这里面函数有main, exit, printf, atoi等,以main函数为例:
在.c中为如下代码:
- int main(int argc,char *argv[]){
- int i;
- if(argc!=4){
- printf("用法: Hello 学号 姓名 秒数!\n");
- exit(1);
- }
- for(i=0;i<8;i++){
- printf("Hello %s %s\n",argv[1],argv[2]);
- sleep(atoi(argv[3]));
- }
- getchar();
- return 0;
- }
在汇编语言中部分代码如下所示:
- main:
- .LFB6:
- .cfi_startproc
- endbr64
- pushq %rbp
- .cfi_def_cfa_offset 16
- .cfi_offset 6, -16
- movq %rsp, %rbp
- .cfi_def_cfa_register 6
- subq $32, %rsp
- movl %edi, -20(%rbp)
- movq %rsi, -32(%rbp)
- cmpl $4, -20(%rbp)
- je .L2
- leaq .LC0(%rip), %rdi
- call puts@PLT
- movl $1, %edi
- call exit@PLT
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
这里面可以看出输入的参数(11,12两行)放在了rdi和rsi中,
main函数是直接被系统函数启动的时候调用的
在下面的代码段的第五行可以看出将返回值0放在eax中,相当于return 0
- .L3:
- cmpl $7, -4(%rbp)
- jle .L4
- call getchar@PLT
- movl $0, %eax
- leave
- .cfi_def_cfa 7, 8
- ret
- .cfi_endproc
3.3.8 类型转换
这里面的atoi函数就是将参数 str 所指向的字符串转换为一个整数(类型为 int 型)是一种典型的显式转换。
在汇编语言中表示为调用atoi函数:
- movq -32(%rbp), %rax
- addq $24, %rax
- movq (%rax), %rax
- movq %rax, %rdi
- call atoi@PLT
- movl %eax, %edi
如上面代码段的第四行,输入参数进入rdi,然后进行转换
3.4 本章小结
本章主要讲述了编译的概念和作用,在Ubuntu系统下如何进行编译操作,以及对于hello的编译结果的解析。更加深入地了解了编译程序是如何通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码的,也就是更加深入地了解了编译器将文本文件 hello.i 翻译成文本文件 hello.s的过程。
第4章 汇编
4.1 汇编的概念与作用
汇编器(assembler)将hello.s转换为hello.o,也就是将汇编代码转换成机器代码的过程就叫做汇编过程。
作用:汇编代码是人较为容易理解的代码,机器仍然难以理解。一个计算机只能理解{0, 1}*的语言。汇编的作用就是将我们得到的.s文件翻译成机器真正能够读懂的二进制代码,从而能够执行我们输入的程序。
4.2 在Ubuntu下汇编的命令
在linux终端输入:
gcc -c hello.s -o hello.o
获得了.o的文件,也就是一种二进制文件,如图:
4.3 可重定位目标elf格式
调用指令readelf -a hello.o > ./elf.txt
产生elf.txt文件:
可执行与可链接格式 (Executable and Linkable Format,缩写 ELF),常被称为 ELF格式,在计算中,是一种用于可执行文件、目标代码、共享库和核心转储的标准文件格式。
(1)ELF头
生成的elf头如下段代码所示:
- ELF 头:
- Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
- 类别: ELF64
- 数据: 2 补码,小端序 (little endian)
- Version: 1 (current)
- OS/ABI: UNIX - System V
- ABI 版本: 0
- 类型: REL (可重定位文件)
- 系统架构: Advanced Micro Devices X86-64
- 版本: 0x1
- 入口点地址: 0x0
- 程序头起点: 0 (bytes into file)
- Start of section headers: 1240 (bytes into file)
- 标志: 0x0
- Size of this header: 64 (bytes)
- Size of program headers: 0 (bytes)
- Number of program headers: 0
- Size of section headers: 64 (bytes)
- Number of section headers: 14
- Section header string table index: 13
elf头中包含整个程序的基本信息,包括了机器的类别(如大端机小端机等),数据的类别,操作系统的类别,系统的架构等信息。
第八行,类型为REL,表明文件的类别是可重定位文件。
(2)节头部表
节头部表如图所示:
- 节头:
- [号] 名称 类型 地址 偏移量
- 大小 全体大小 旗标 链接 信息 对齐
- [ 0] NULL 0000000000000000 00000000
- 0000000000000000 0000000000000000 0 0 0
- [ 1] .text PROGBITS 0000000000000000 00000040
- 0000000000000092 0000000000000000 AX 0 0 1
- [ 2] .rela.text RELA 0000000000000000 00000388
- 00000000000000c0 0000000000000018 I 11 1 8
- [ 3] .data PROGBITS 0000000000000000 000000d2
- 0000000000000000 0000000000000000 WA 0 0 1
- [ 4] .bss NOBITS 0000000000000000 000000d2
- 0000000000000000 0000000000000000 WA 0 0 1
- [ 5] .rodata PROGBITS 0000000000000000 000000d8
- 0000000000000033 0000000000000000 A 0 0 8
- [ 6] .comment PROGBITS 0000000000000000 0000010b
- 000000000000002c 0000000000000001 MS 0 0 1
- [ 7] .note.GNU-stack PROGBITS 0000000000000000 00000137
- 0000000000000000 0000000000000000 0 0 1
- [ 8] .note.gnu.propert NOTE 0000000000000000 00000138
- 0000000000000020 0000000000000000 A 0 0 8
- [ 9] .eh_frame PROGBITS 0000000000000000 00000158
- 0000000000000038 0000000000000000 A 0 0 8
- [10] .rela.eh_frame RELA 0000000000000000 00000448
- 0000000000000018 0000000000000018 I 11 9 8
- [11] .symtab SYMTAB 0000000000000000 00000190
- 00000000000001b0 0000000000000018 12 10 8
- [12] .strtab STRTAB 0000000000000000 00000340
- 0000000000000048 0000000000000000 0 0 1
- [13] .shstrtab STRTAB 0000000000000000 00000460
- 0000000000000074 0000000000000000 0 0 1
- Key to Flags:
- W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
- L (link order), O (extra OS processing required), G (group), T (TLS),
- C (compressed), x (unknown), o (OS specific), E (exclude),
- l (large), p (processor specific)
该表描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
(3)重定位节
如下图所示:
- 重定位节 '.rela.text' at offset 0x388 contains 8 entries:
- 偏移量 信息 类型 符号值 符号名称 + 加数
- 00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
- 000000000021 000c00000004 R_X86_64_PLT32 0000000000000000 puts - 4
- 00000000002b 000d00000004 R_X86_64_PLT32 0000000000000000 exit - 4
- 000000000054 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 22
- 00000000005e 000e00000004 R_X86_64_PLT32 0000000000000000 printf - 4
- 000000000071 000f00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
- 000000000078 001000000004 R_X86_64_PLT32 0000000000000000 sleep - 4
- 000000000087 001100000004 R_X86_64_PLT32 0000000000000000 getchar - 4
- 重定位节 '.rela.eh_frame' at offset 0x448 contains 1 entry:
- 偏移量 信息 类型 符号值 符号名称 + 加数
- 000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
- The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
偏移量:通常对应于一些需要重定向的程序代码所在.text或.data节中的偏移位置.
信息:重定位到的目标在符号表中的偏移量
类型:代表重定位的类型,与信息相互对应。
名称:重定向到的目标的名称
加数:用来作为计算重复或定位文件位置的一个辅助运算信息,共计约占8个字节。
注:本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。重定位节.rela.text ,一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。偏移量是需要被修改的引用的节偏移,符号标识被修改引用应该指向的符号。类型告知链接器如何修改新的引用,加数是一个有符号常数,一些类型的重定位要用它对被修改引用的值做偏移调整。
(4)Symbol table
- Symbol table '.symtab' contains 18 entries:
- Num: Value Size Type Bind Vis Ndx Name
- 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
- 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
- 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
- 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
- 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
- 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
- 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
- 7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
- 8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
- 9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
- 10: 0000000000000000 146 FUNC GLOBAL DEFAULT 1 main
- 11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
- 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
- 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
- 14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
- 15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi
- 16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
- 17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar
Symbol table用于存放程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
输入objdump -d -r hello.o之后,如下图所示:
- hello.o: 文件格式 elf64-x86-64
- Disassembly of section .text:
- 0000000000000000 <main>:
- 0: f3 0f 1e fa endbr64
- 4: 55 push %rbp
- 5: 48 89 e5 mov %rsp,%rbp
- 8: 48 83 ec 20 sub $0x20,%rsp
- c: 89 7d ec mov %edi,-0x14(%rbp)
- f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
- 13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
- 17: 74 16 je 2f <main+0x2f>
- 19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
- 1c: R_X86_64_PC32 .rodata-0x4
- 20: e8 00 00 00 00 callq 25 <main+0x25>
- 21: R_X86_64_PLT32 puts-0x4
- 25: bf 01 00 00 00 mov $0x1,%edi
- 2a: e8 00 00 00 00 callq 2f <main+0x2f>
- 2b: R_X86_64_PLT32 exit-0x4
- 2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
- 36: eb 48 jmp 80 <main+0x80>
- 38: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 3c: 48 83 c0 10 add $0x10,%rax
- 40: 48 8b 10 mov (%rax),%rdx
- 43: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 47: 48 83 c0 08 add $0x8,%rax
- 4b: 48 8b 00 mov (%rax),%rax
- 4e: 48 89 c6 mov %rax,%rsi
- 51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58 <main+0x58>
- 54: R_X86_64_PC32 .rodata+0x22
- 58: b8 00 00 00 00 mov $0x0,%eax
- 5d: e8 00 00 00 00 callq 62 <main+0x62>
- 5e: R_X86_64_PLT32 printf-0x4
- 62: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 66: 48 83 c0 18 add $0x18,%rax
- 6a: 48 8b 00 mov (%rax),%rax
- 6d: 48 89 c7 mov %rax,%rdi
- 70: e8 00 00 00 00 callq 75 <main+0x75>
- 71: R_X86_64_PLT32 atoi-0x4
- 75: 89 c7 mov %eax,%edi
- 77: e8 00 00 00 00 callq 7c <main+0x7c>
- 78: R_X86_64_PLT32 sleep-0x4
- 7c: 83 45 fc 01 addl $0x1,-0x4(%rbp)
- 80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
- 84: 7e b2 jle 38 <main+0x38>
- 86: e8 00 00 00 00 callq 8b <main+0x8b>
- 87: R_X86_64_PLT32 getchar-0x4
- 8b: b8 00 00 00 00 mov $0x0,%eax
- 90: c9 leaveq
- 91: c3 retq
1. 可以看出反汇编的结果包含了机器代码以及后面的汇编代码,而hello.s仅仅是汇编代码。
2. 在分支转移函数调用方面,反汇编结果相对于hello.s的.L1等跳转表式地址或者是直接是call + 函数名称来说,使用的是函数的相对偏移位置,因为只有在hello.o在链接之后才会知道每一个函数的绝对位置,所以在.rela.text中补充重定位的条目。
3. 在操作数方面,反汇编的结果使用的数字是16进制,而hello.s使用的是10十进制操作数。
4.5 本章小结
本章阐释了汇编的概念与作用,说明了ubuntu操作系统下使用汇编操作将hello.s转换成hello.o的方法,了解并解释了可重定位目标文件ELF,通过调用指令生成elf.txt,对文件中的elf头,节头部表,重定位节,symbol table等内容进行分析,获取系统、程序的信息。通过对于hello.o的反汇编程序与hello.s的关系,更加深入地理解编译和汇编等问题。
第5章 链接
5.1 链接的概念与作用
链接有几种:
静态链接:(背景:几个.c文件经过cpp预处理,cc1进行编译,as进行汇编称为.o文件,生成几个可重定位目标文件)链接器先进行符号解析,然后对于地址进行重定位,将多个文件合成为一个可执行文件的过程。
作用:有利于程序之间的模块化,所以函数都可以分开编译,提高时间效率,节约空间,使一个大型的工程能够进行任务分配,实现多个文件生成一个单一可执行文件的过程。
5.2 在Ubuntu下链接的命令
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.o
生成hello文件如下图所示
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
输入指令:readelf -a hello > hello1.elf
生成了hello1.elf文件
(1)ELF头如下图
- ELF 头:
- Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
- 类别: ELF64
- 数据: 2 补码,小端序 (little endian)
- Version: 1 (current)
- OS/ABI: UNIX - System V
- ABI 版本: 0
- 类型: EXEC (可执行文件)
- 系统架构: Advanced Micro Devices X86-64
- 版本: 0x1
- 入口点地址: 0x4010f0
- 程序头起点: 64 (bytes into file)
- Start of section headers: 14208 (bytes into file)
- 标志: 0x0
- Size of this header: 64 (bytes)
- Size of program headers: 56 (bytes)
- Number of program headers: 12
- Size of section headers: 64 (bytes)
- Number of section headers: 27
- Section header string table index: 26
elf头中包含整个程序的基本信息,包括了机器的类别(如大端机小端机等),数据的类别,操作系统的类别,系统的架构等信息。
(2)节头
- 节头:
- [号] 名称 类型 地址 偏移量
- 大小 全体大小 旗标 链接 信息 对齐
- [ 0] NULL 0000000000000000 00000000
- 0000000000000000 0000000000000000 0 0 0
- [ 1] .interp PROGBITS 00000000004002e0 000002e0
- 000000000000001c 0000000000000000 A 0 0 1
- [ 2] .note.gnu.propert NOTE 0000000000400300 00000300
- 0000000000000020 0000000000000000 A 0 0 8
- [ 3] .note.ABI-tag NOTE 0000000000400320 00000320
- 0000000000000020 0000000000000000 A 0 0 4
- [ 4] .hash HASH 0000000000400340 00000340
- 0000000000000038 0000000000000004 A 6 0 8
- [ 5] .gnu.hash GNU_HASH 0000000000400378 00000378
- 000000000000001c 0000000000000000 A 6 0 8
- [ 6] .dynsym DYNSYM 0000000000400398 00000398
- 00000000000000d8 0000000000000018 A 7 1 8
- [ 7] .dynstr STRTAB 0000000000400470 00000470
- 000000000000005c 0000000000000000 A 0 0 1
- [ 8] .gnu.version VERSYM 00000000004004cc 000004cc
- 0000000000000012 0000000000000002 A 6 0 2
- [ 9] .gnu.version_r VERNEED 00000000004004e0 000004e0
- 0000000000000020 0000000000000000 A 7 1 8
- [10] .rela.dyn RELA 0000000000400500 00000500
- 0000000000000030 0000000000000018 A 6 0 8
- [11] .rela.plt RELA 0000000000400530 00000530
- 0000000000000090 0000000000000018 AI 6 21 8
- [12] .init PROGBITS 0000000000401000 00001000
- 000000000000001b 0000000000000000 AX 0 0 4
- [13] .plt PROGBITS 0000000000401020 00001020
- 0000000000000070 0000000000000010 AX 0 0 16
- [14] .plt.sec PROGBITS 0000000000401090 00001090
- 0000000000000060 0000000000000010 AX 0 0 16
- [15] .text PROGBITS 00000000004010f0 000010f0
- 0000000000000145 0000000000000000 AX 0 0 16
- [16] .fini PROGBITS 0000000000401238 00001238
- 000000000000000d 0000000000000000 AX 0 0 4
- [17] .rodata PROGBITS 0000000000402000 00002000
- 000000000000003b 0000000000000000 A 0 0 8
- [18] .eh_frame PROGBITS 0000000000402040 00002040
- 00000000000000fc 0000000000000000 A 0 0 8
- [19] .dynamic DYNAMIC 0000000000403e50 00002e50
- 00000000000001a0 0000000000000010 WA 7 0 8
- [20] .got PROGBITS 0000000000403ff0 00002ff0
- 0000000000000010 0000000000000008 WA 0 0 8
- [21] .got.plt PROGBITS 0000000000404000 00003000
- 0000000000000048 0000000000000008 WA 0 0 8
- [22] .data PROGBITS 0000000000404048 00003048
- 0000000000000004 0000000000000000 WA 0 0 1
- [23] .comment PROGBITS 0000000000000000 0000304c
- 000000000000002b 0000000000000001 MS 0 0 1
- [24] .symtab SYMTAB 0000000000000000 00003078
- 00000000000004c8 0000000000000018 25 30 8
- [25] .strtab STRTAB 0000000000000000 00003540
- 0000000000000158 0000000000000000 0 0 1
- [26] .shstrtab STRTAB 0000000000000000 00003698
- 00000000000000e1 0000000000000000 0 0 1
- Key to Flags:
- W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
- L (link order), O (extra OS processing required), G (group), T (TLS),
- C (compressed), x (unknown), o (OS specific), E (exclude),
- l (large), p (processor specific)
该表描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在edb中加载hello虚拟地址信息如上图所示
查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。
对比5.4.3中的elf文件结构:elf头+节头等,data dump同样可以查看,包含PHDR,INTERP,LOAD ,DYNAMIC,NOTE ,GNU_STACK,GNU_RELRO等部分。
5.5 链接的重定位过程分析
先使用objdump -d -r hello得到hello的反汇编,再使用objdump -d -r hello.o得到.o文件的反汇编文件。
(1)hello的反汇编,相对于hello.o的反汇编增加了很多节以及函数,例如.init和.plt和一些节中的函数等等
(2)hello的反汇编中的地址是有明确的地址的,而hello.o的有些部分则还是地址为一串0,原因是hello.o没有完成重定位的过程,hello已经完成了重定位,因此hello.o中为0串的地址再hello中已经被替换成了一个非零的地址。
如下图所示:
Hello.o反汇编部分程序:
- Disassembly of section .text:
- 0000000000000000 <main>:
- 0: f3 0f 1e fa endbr64
- 4: 55 push %rbp
- 5: 48 89 e5 mov %rsp,%rbp
- 8: 48 83 ec 20 sub $0x20,%rsp
- c: 89 7d ec mov %edi,-0x14(%rbp)
- f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
- 13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
- 17: 74 16 je 2f <main+0x2f>
- 19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
- 1c: R_X86_64_PC32 .rodata-0x4
- 20: e8 00 00 00 00 callq 25 <main+0x25>
- 21: R_X86_64_PLT32 puts-0x4
- 25: bf 01 00 00 00 mov $0x1,%edi
- 2a: e8 00 00 00 00 callq 2f <main+0x2f>
- 2b: R_X86_64_PLT32 exit-0x4
- 2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
- 36: eb 48 jmp 80 <main+0x80>
- 38: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 3c: 48 83 c0 10 add $0x10,%rax
- 40: 48 8b 10 mov (%rax),%rdx
- 43: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 47: 48 83 c0 08 add $0x8,%rax
- 4b: 48 8b 00 mov (%rax),%rax
- 4e: 48 89 c6 mov %rax,%rsi
- 51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58 <main+0x58>
- 54: R_X86_64_PC32 .rodata+0x22
- 58: b8 00 00 00 00 mov $0x0,%eax
- 5d: e8 00 00 00 00 callq 62 <main+0x62>
- 5e: R_X86_64_PLT32 printf-0x4
- 62: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 66: 48 83 c0 18 add $0x18,%rax
- 6a: 48 8b 00 mov (%rax),%rax
- 6d: 48 89 c7 mov %rax,%rdi
- 70: e8 00 00 00 00 callq 75 <main+0x75>
- 71: R_X86_64_PLT32 atoi-0x4
- 75: 89 c7 mov %eax,%edi
- 77: e8 00 00 00 00 callq 7c <main+0x7c>
- 78: R_X86_64_PLT32 sleep-0x4
- 7c: 83 45 fc 01 addl $0x1,-0x4(%rbp)
- 80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
- 84: 7e b2 jle 38 <main+0x38>
- 86: e8 00 00 00 00 callq 8b <main+0x8b>
- 87: R_X86_64_PLT32 getchar-0x4
- 8b: b8 00 00 00 00 mov $0x0,%eax
- 90: c9 leaveq
- 91: c3 retq
Hello反汇编部分程序:
- Disassembly of section .init:
- 0000000000401000 <_init>:
- 401000: f3 0f 1e fa endbr64
- 401004: 48 83 ec 08 sub $0x8,%rsp
- 401008: 48 8b 05 e9 2f 00 00 mov 0x2fe9(%rip),%rax # 403ff8 <__gmon_start__>
- 40100f: 48 85 c0 test %rax,%rax
- 401012: 74 02 je 401016 <_init+0x16>
- 401014: ff d0 callq *%rax
- 401016: 48 83 c4 08 add $0x8,%rsp
- 40101a: c3 retq
- Disassembly of section .plt:
- 0000000000401020 <.plt>:
- 401020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
- 401026: f2 ff 25 e3 2f 00 00 bnd jmpq *0x2fe3(%rip) # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
- 40102d: 0f 1f 00 nopl (%rax)
- 401030: f3 0f 1e fa endbr64
- 401034: 68 00 00 00 00 pushq $0x0
- 401039: f2 e9 e1 ff ff ff bnd jmpq 401020 <.plt>
- 40103f: 90 nop
- 401040: f3 0f 1e fa endbr64
- 401044: 68 01 00 00 00 pushq $0x1
- 401049: f2 e9 d1 ff ff ff bnd jmpq 401020 <.plt>
- 40104f: 90 nop
- 401050: f3 0f 1e fa endbr64
- 401054: 68 02 00 00 00 pushq $0x2
- 401059: f2 e9 c1 ff ff ff bnd jmpq 401020 <.plt>
- 40105f: 90 nop
- 401060: f3 0f 1e fa endbr64
- 401064: 68 03 00 00 00 pushq $0x3
- 401069: f2 e9 b1 ff ff ff bnd jmpq 401020 <.plt>
- 40106f: 90 nop
- 401070: f3 0f 1e fa endbr64
- 401074: 68 04 00 00 00 pushq $0x4
- 401079: f2 e9 a1 ff ff ff bnd jmpq 401020 <.plt>
- 40107f: 90 nop
- 401080: f3 0f 1e fa endbr64
- 401084: 68 05 00 00 00 pushq $0x5
- 401089: f2 e9 91 ff ff ff bnd jmpq 401020 <.plt>
- 40108f: 90 nop
重定位过程的分析:
链接器将不同程序相同类型的节放在一起,并且将hello.c的库函数链接进了hello之中,并且将地址转换为内存中的地址,有两种方式:绝对地址和PC相对寻址,重定位算法如下图所示:
5.6 hello的执行流程
下面列出各个子程序名:
_dl_start
_dl_init
_start
__libc_start_main
__cxa_atexit
__libc_csu_init
_init
_setjmp
_sigsetjmp
__sigjmp_save
main
puts@plt
exit@plt
printf@plt
atoi@plt
sleep@plt
getchar@plt
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
exit
5.7 Hello的动态链接分析
编译器在动态链接的过程中需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。
di_init运行之前,使用data dump查看对应的地址内容
发现是全零的地址,也就是没进行重定位
di_init运行之后,使用data dump查看对应的地址内容 发现原来全0的一行变成了非0的地址,也就是进行了重定位处理。
5.8 本章小结
本章复习了链接的概念以及作用,并且实现了ubuntu下的链接操作,通过转换成elf格式对比未链接的可执行文件,得出链接的操作特点,并且通过EDB的查看进行了hello流程的分析以及动态链接的分析。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间。用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程。
作用:进程可以给我们一个假象:程序无间断处理一条指令,程序独占处理器和内存。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell是一种应用程序,它为用户访问操作系统内核提供了一个交互界面。用户可以通过shell向操作系统发出请求,操作系统选择执行命令。
处理流程:先在shell中输入指令,然后shell程序使用praseline builtin函数进行字符串划分,获取命令。若得到的命令为内置命令,那么立即执行,如果不是内置命令那么就调用可执行文件fork一个子进程。
6.3 Hello的fork进程创建过程
在命令行输入./hello命令,然后shell程序使用praseline builtin函数进行字符串划分,获取命令。经过判断发现不是系统的内置命令,因此通过fork函数创建子进程,子进程得到一份与父进程用户级虚拟空间相同且独立的副本(包括数据段、代码、共享库、堆和用户栈)。
6.4 Hello的execve过程
Execve在当前进程中载入并运行程序,函数形式为int execve(char *filename, char *argv[], char *envp[])。覆盖当前进程的代码、数据、栈,保留:有相同的PID,继承已打开的文件描述符和信号上下文。调用一次并从不返回,除非有错误,例如:指定的文件不存在。
6.5 Hello的进程执行
用户模式和内核模式:
处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文:
上下文是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
调度:
当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行如下操作:
(1)保存以前进程的上下文
(2)恢复新恢复进程被保存的上下文
(3)将控制传递给这个新恢复的进程,来完成上下文切换。
如上图所示。
6.6 hello的异常与信号处理
异常有四种:
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
6.7本章小结
本章内容介绍了进程的概念和作用,shell-bash的作用和处理过程,fork,execve以及进程执行等过程,又通过实际操作了解了各种信号的作用和产生结果。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
Hello.c程序经过预处理,将#include、宏定义、预编译指令如#if, #elif等展开或处理插入原文件,删除所有注释,添加行号信息文件名信息,便于调试,并规定某段代码的条件与限制,得到hello.i文件作为文件扩展名的C程序。然后使用编译器将文本文件hello.i编译成文本文件hello.s。之后通过汇编器将hello.s转换为hello.o,得到真正能够被机器读懂的二进制代码文件。接着使用连接器先进行符号解析,然后对于地址进行重定位,将多个文件合成为一个可执行文件。从而实现了多个文件合成为一个可执行文件的过程。在运行程序的时候,我们在shell中输入./hello,shell程序然后shell程序使用praseline builtin函数进行字符串划分,获取命令。若得到的命令为内置命令,那么立即执行,如果不是内置命令那么就调用可执行文件fork一个子进程。此时,./hello使shell程序fork了一个子进程,通过各种输入的异常信号控制程序的运行。通过hello的存储管理和I/O管理保证程序正常的运行。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
我的感悟:计算机系统对于一个程序的控制原来是如此复杂的,我们每输入一个简单的指令都需要调用计算机的软件和硬件进行配合工作。每一个看似很简单的操作背后都是多少年来数学家、计算机科学家抽象包装好的,才能够有这样程序员友好的编程环境、平台。但是程序员仍然需要了解计算机系统的内部构造,这样才能够编写出底层软硬件友好的程序。
创新理念:可以设计一个软件,用于显示出计算机系统内部为了编译运行一个程序而进行的工作,这样可以有利于新手学习计算机系统时的理解。
附件
文件名 | 作用 |
hello.c | 储存C语言源代码 |
hello.i | 预处理生成的文件 |
hello.s | 编译生成的文本文件 |
hello.o | 汇编器生成的二进制文件 |
elf.txt | hello.o文件的可执行可链接文件的txt文件,包含hello.o的信息 |
hello | 链接操作生成的可执行文件 |
hello1.elf | hello文件的可执行可链接文件,包含hello的信息 |
参考文献
[1] 《深入理解计算机系统》Bryant and O’hallaron.
[2] Global Descriptor Table - Wikipedia.
[3] Translation lookaside buffer - Wikipedia