计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 -
班 级 -
学 生 符世博
指 导 教 师 -
计算机科学与技术学院
2022年11月
本文讲述了hello的一生,从源代码到预处理文件,从预处理文件到汇编文件,从汇编文件到可重定位目标文件,从可重定位目标文件到可执行文件以及加载到内存中运行到终止的全过程。
关键词:计算机系统;预处理;编译;汇编;链接;
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
第1章 概述
1.1 Hello简介
P2P:from program to process,即从程序到进程。在Linux系统中。hello.c文件经过预处理生成hello.i文件、再经过编译生成hello.s、之后经过汇编生成hello.o文件、最后通过链接生成可执行程序hello文件。之后在shell中输入命令./hello,shell调用系统内核的进程管理为其创建子进程。
020:from zero to zero。Hello程序在执行前不占用内存空间。再shell为其创建子进程后,首先调用execve,载入虚拟内存和物理内存。之后操作系统为这个进程分时间片。当该进程时间片到达时,操作系统设置上下文,并跳转到程序开始处。之后进入主程序执行代码,调用各种系统函数实现输出信息等功能。最终程序结束,shell回收此进程,释放其占用的资源。
1.2 环境与工具
- 硬件环境
CPU: Intel® Core™ i7-8565U CPU @ 1.80GHz
RAM: 16.00GB
- 软件环境
Windows10 64位
Oracle VM VirtualBox 6.1.16 r140961 (Qt5.6.2)
Ubuntu 20.04.1
- 开发与调试工具
Visual Studio Code
cpp(预处理器)
gcc(编译器)
as(汇编器)
ld(链接器)
GNU readelf
GNU gdb
EDB等
1.3 中间结果
hello.i | 预处理文件 |
hello.o | 可重定位目标文件 |
hello.s | 汇编文件 |
hello_o_dis.txt | hello.o反汇编结果 |
hello_elf.txt | hello中的elf信息结果 |
hello_dis.txt | hello反汇编结果 |
hello_o_elf.txt | hello.o中的elf信息 |
hello | 可执行目标文件 |
1.4 本章小结
本章介绍了P2P、020的概念,之后给出了所使用的软硬件环境以及使用的工具,最后介绍了完成本论文所用到的所有中间文件。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理是C语言的一个重要的功能,它由预处理程序负责完成,它可以进行代码文本的替换工作,同时还会删除程序中的注释和多余的空白字符,但不会去做文本检查。最后将修改的程序保存成,i文件。
预处理的作用:预处理的作用主要可以分成以下三部分:
1.将源文件中以include格式包含的文件复制到编译的源文件中。
2.用实际值替换用#define定义的字符串。
3.根据#if后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
使用gcc -E hello.c -o hello.imin命令可以对hello.c文件进行预处理,并将结果输出到hello.i文件。
图 2-1预处理命令截图
2.3 Hello的预处理结果解析
打开生成的.i文件,发现原本23行的.c文件经过预处理后拓展成了3060行的.i文件。
文件的开始是源代码文件的一些信息,如下图:
图 2-2预处理结果截图
之后是预处理拓展的内容,部分内容如下图:
图 2-3预处理结果截图
图 2-4预处理结果截图
最后是hello.c中的源代码部分,除去了注释和“include”语句,如下图:
图 2-5预处理结果截图
那么.i文件中那么多的拓展文本是从哪里来的呢?这就要说到预处理的过程了。
由于预处理只是对源代码中以“#”开头的语句进行处理,因此在预处理阶段中程序定义的其他操作不会进行处理。由于在程序中有关于头文件的文件包含,因此在预处理时对这一段进行了解析。以“stdio.h”为例:由于它使用<>进行引用的,所以gcc会到Linux系统的环境变量下寻找stdio.h,在/usr/include目录下找到stdio.h并打开,发现其中也使用了“#define”和“#include”,如下图,所以会对这些东西进行递归的展开并替换,而最终的.i文件会删除这些东西。而对于文件使用的“#ifdef”、“#ifndef”等语句,gcc会对条件值进行判断来决定是否包含其中的内容。
图 2-6sdtdio.h文件截图
2.4 本章小结
本章介绍了hello.c的预处理过程,并对产生的hello.i文件进行了分析。
第3章 编译
3.1 编译的概念与作用
编译的概念:编译是指对预处理后的代码进行检查和分析,确保所有语句符合规则并将其翻译成汇编代码的过程。
编译的作用:
将预处理完成后的代码进行一系列的分析和优化后生成相应的汇编代码文件。
3.2 在Ubuntu下编译的命令
使用gcc -S hello.i -o hello.s命令可以对hello.i文件进行编译,并将结果输出值hello.s文件。
图 3-1编译命令截图
3.3 Hello的编译结果解析
3.3.1数据
3.3.1.1常量:
在这个程序中,常量数据有printf输出的格式串,它们被保存在了.rodata段
hello.c:
14行 printf("用法: Hello 学号 姓名 秒数!\n");
18行 printf("Hello %s %s\n",argv[1],argv[2]);
这两个输出对应的格式串被储存在了helloc.s的5-8行:
.LC0:
.string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"
.LC1:
.string "Hello %s %s\n"
同时还有一些以立即数的形式在汇编代码中出现,比如在.c文件中的13行的if判断语句中比较argc与4的关系,这里的这个4在汇编代码中就是以立即数的形式出现的。
hello.s:
24行 cmpl $4, -20(%rbp)
3.3.1.2变量:
在.c文件中有一个局部变量int i,在一个循环体中作为控制变量,在汇编代码中,它被存储在了栈中。
hello.c
11行 int i;
hello.s
53行 cmpl $8, -4(%rbp)
其他的变量也用类似的方式进行表示。
3.3.2算术操作
在for循环中每次对变量i进行++运算,在汇编代码中使用了addl指令进行这个操作。
hello.c
17行 for(i=0;i<9;i++)
hello.s
51行 addl $1, -4(%rbp)
3.3.3数组/指针操作
在main函数的参数中有一个字符串数组char *argv[]。在汇编代码中,使用栈中的连续空间进行存储。
hello.c
10行 int main(int argc,char *argv[])
hello.s
23行 movq %rsi, -32(%rbp)
这样第二个参数就被写入了栈空间中,并且如果传入了4个参数(即没有进入第一个if),观察后面调用的printf和sleep函数对应的汇编代码可以发现,第4个参数存储在了%rbp-8 指向的栈空间,第三个和第二个分别存在了%rbp-16和%rbp-24指向的栈空间中。
3.3.4控制转移
以main函数中的if和for语句为例,在if语句中判断了argc与4的关系,如果相等执行后面的代码块,否则不执行,这在汇编代码中使用了cmpl和je完成
hello.c
13行 if(argc!=4)
hello.s
24-25行 cmpl $4, -20(%rbp)
je .L2
在for语句中变量i从0开始,每次循环加1,直到i<8不满足为止。在汇编代码中使用cmpl指令,判断i是否小于8,是则继续执行,否则跳出循环。
hello.c
17行 for(i=0;i<9;i++)
hello.s
31行 movl $0, -4(%rbp) 赋初值
51行 addl $1, -4(%rbp) 增1
53-54行 cmpl $8, -4(%rbp) 判断循环是否继续
jle .L4
3.3.5函数操作
main函数有两个参数,在一开始由edi和rsi进行传送
hello.s:
22-23行 movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
返回值由eax寄存器传递,在参数正确时返回0,否则返回1。
hello.s:
28行 movl $1, %edi
56行 movl $0, %eax
在main函数中调用的函数有printf,exit,atoi,sleep,getchar函数
首先是printf函数,在参数传递时,第一个只需要传递一个输出字符串对应汇编代码如下
hello.s:
26行 leaq .LC0(%rip), %rdi
而第二个还需要传入对应的参数argv[1]和argv[2],对应的汇编代码如下
hello.s:
34-43行 movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
返回值在main函数中没有被使用
之后是exit函数,传入的参数时退出的状态值,对应的汇编代码如下
hello.s:
28行 movl $1, %edi
没有返回值
接着是atoi函数,传入的参数为字符串首地址,对应的汇编代码如下
hello.s:
44-48行 movq -32(%rbp), %rax
addq $24, %rax argv[3]的地址
movq (%rax), %rax 将argv[3]的字符串首地址传送到rax中
movq %rax, %rdi
call atoi@PLT
返回值是对应的整数值,存放在eax寄存器中
之后是sleep函数,它使用atoi的返回值作为参数,返回值在main函数中不被使用,对应的汇编代码如下
hello.s
49-50行 movl %eax, %edi
call sleep@PLT
最后是getchar函数,没有参数传递,返回值也不被使用,所以在汇编代码中只用了一条call指令。
3.4 本章小结
本章介绍了从.i文件编译到.s文件的过程,并对.s文件进行了分析
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编是指将汇编语言书写的程序翻译成等价的机器语言程序的过程。汇编器将.s文件翻译成机器语言指令,然后把这些指令打包成可重定位程序的格式,并将结果输出为可重定位目标文件hello.o。
汇编的作用
将hello.s 翻译成CPU语言指令,把这些指令打包成可重定位目标程序的格式,并且将结果保存在二进制目标文件hello.o当中。
4.2 在Ubuntu下汇编的命令
使用gcc -c hello.s -o hello.o命令可以对hello.s文件进行汇编,并将结果输出到hello.o文件
图 4-1汇编命令截图
4.3 可重定位目标elf格式
使用readelf -a hello.o > hello_o_elf.txt命令可以将hello.o中ELF格式相关信息重定向到hello_o_elt.txt文件。
首先是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
第一行的magic用来表示ELF文件,7f 45 4c 46分别对应ASCII码的Del E L F,其余的用来标识为位数,大端/小端序,版本号等,后九个未定义。剩下的部分包括了ELF头大小,目标文件类型,机器类型,字节头部表的文件偏移和节头部表中的条目大小和数量等信息。
节头部表
节头部表给出了文件中14个节的名称、类型、地址、偏移量和大小等信息。如下:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 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)
There are no section groups in this file.
本文件中没有程序头。
There is no dynamic section in this file.
由于这是可重定位目标文件,所以每个节都从0开始。并且还描述了每个节的读写权限。
重定位节
这一部分记录了每一段中引用的符号的相关信息,在链接时,通过重定位节对这些地址进行重定位。如下
重定位节 '.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
符号表
这一部分用于存放定义和引用的函数和全局变量信息,如下
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
4.4 Hello.o的结果解析
使用objdump -d -r hello.o > hello_dis.txt命令对hello.o进行反汇编,并将结果重定向到hello_dis.txt文件中。
反汇编结果如下:
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 08 cmpl $0x8,-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.o文件中,操作数改成了十六进制表示。并且,printf输出的字符串也被替换成了待重定位的地址。同时,在使用call指令时,.o文件使用的是待链接器重定位的相对偏移地址,而不是使用.s文件中使用的函数名。还有,对于跳转的位置,使用了相对于main函数起始位置偏移的地址来进行跳转。
4.5 本章小结
本章对汇编的过程进行了介绍,并分析了hello.o中 ELF 头、节头部表、重定位节以及符号表。比较了hello.o和hello.s文件的区别。
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是指将可重定位目标文件经过符号解析和重定位步骤合并成可执行目标文件的过程。
链接的作用:使得一个项目可以被分解成许多小模块,每个模块可单独进行修改、更新,最后通过链接形成一个项目时,减少不必要的操作。
5.2 在Ubuntu下链接的命令
使用ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello命令对hello.o文件进行连接,生成可执行目标文件hello。
图 5-1链接命令截图
5.3 可执行目标文件hello的格式
使用readelf -a hello > hello_elf.txt命令可将hello中ELF格式信息重定向至hello_elf.txt文件。
各段的起始地址、大小信息如下所示
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 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
5.4 hello的虚拟地址空间
使用edb加载hello,如下图
图 5-2使用edb加载hello
可以看到hello的虚拟地址起始于0x40100
通过与Symbols窗口,可以查看各段对应的信息与虚拟地址,如下图
图 5-3与symbols对照(部分)
图5-4与symbols对照(部分)
5.5 链接的重定位过程分析
使用objdump -d -r hello > hello_dis.txt命令,将hello的反汇编代码重定向至hello_dis.txt文件。
分析可知,在hello中包含了外部库代码,有了新的节,不再有重定位条目,并且跳转地址也变成了虚拟地址。
链接的过程:链接器会扫描分析所有可重定位目标文件,并完成符号解析和重定位。首先是符号解析,将每个符号引用于一个符号定义关联起来;之后是重定位,在重定位之前,链接器会把所有的文件进行合并,然后将运行时的虚拟地址重新赋给每个节,之后链接器通过编译器产生的重定位条目进行分析,把每个符号定义和一个内存位置关联起来。最后产生一个所需的可执行目标文件,
5.6 hello的执行流程
使用gdb进行调试。根据前面得到的ELF信息,可以知道函数入口在0x4010f0,所以在执行程序时,首先从_start开始执行,所以在这里打上断点。
图 5-5gdb调试
之后单步执行,发现首先调用的是__libc_start_main这个函数,它用来向main函数传递参数。
图 5-6函数调用
之后又调用了 __GI___cxa_atexit,__lll_cas_lock,__new_exitfn,之后又回到了__libc_start_main。
图 5-7函数调用
之后调用__libc_csu_init函数,然后调用init再返回,接着再返回__libc_start_main。
图 5-8函数调用
图 5-9函数调用
之后调用_setjmp、_sigsetjmp和_sigjmp_save,然后返回到__libc_start_main,最后来到了main函数中。
图 5-10函数调用
在main函数中,根据输入,调用不同的函数,比如我这里就没有输入,之后调用puts@plt,之后再调用__GI__IO_puts,再调用一系列的函数,返回到__GI__IO_puts
图 5-11函数调用
图 5-12函数调用
图 5-13函数调用
之后__GI__IO_puts返回到main函数,接着调用exit。
之后调用了__GI_exit再调用__run_exit_handlers,__GI___call_tls_dtors之后返回到__run_exit_handlers,再调用__GI___call_tls_dtors,再回到 __run_exit_handlers,然后调用 _IO_cleanup,再调用 _IO_flush_all_lockp,最后回到__run_exit_handlers,再调用__GI_exit,回到main函数。
5.7 Hello的动态链接分析
在加载hello时,动态链接器对共享目标文件中的相应的代码和数据进行重定位,加载共享库,生成完全的可执行目标文件。
使用edb查看.got.plt段的内容,在未调用dl_init时,内容如下;
图 5-14未调用dl_init时.got.plt段的内容
调用之后的内容如下:
图 5-15调用dl_init后.got.plt段的内容
可以看出在调用前后对应的内容发生了变化。
延迟绑定通过GOT和PLT实现,其中GOT存放函数地址,PLT使用GOT中的地址跳转到目标函数。当程序调用共享库定义的函数时,编译器无法预测这个函数运行时的地址因为定义它的共享模块可以在运行时加载到任意地址。所以,在调用共享库函数时,会首先跳转到PLT执行指令,第一次跳转时,GOT条目为PLT下一条指令,首先将函数ID压栈,然后跳转到PLT[0],之后在PLT[0]再将重定位表地址压栈,然后转进动态链接器,在动态链接器中使用两个栈条目确定函数运行时地址重写GOT,再将控制传递给目标函数。在之后的运行过程中如果再次调用同一函数,则通过间接跳转将控制直接转移至目标函数。
5.8 本章小结
本章首先说明了hello.o重定位生成可执行目标文件hello的过程,之后说明了hello的虚拟地址空间与节头部表的对应关系,接着分析了hello的执行过程并对hello进行了动态链接分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程的经典定义是一个执行中程序的实例,是操作系统对一个正在运行的程序的一种抽象。
进程的作用:每次用户通过shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
进程提供给应用程序的两个抽象:一个独立的逻辑控制流,好像程序独占地使用处理器;一个私有的地址空间,好像程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:Shell是一个交互型应用级程序,代表用户运行其他程序。它是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。
Shell的处理流程:shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序。然后shell在搜索路径里寻找这些应用程序(。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。
6.3 Hello的fork进程创建过程
首先,在shell界面输入命令 ./hello 2021113140 符世博 1之后,shell会判断输入的是否为内置命令,若不是再检查是否是一个应用程序。在当前目录下找到了hello,之后通过fork创建子进程。子进程与父进程相似,会得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈等,父进程打开的文件,子进程也可读写。二者之间最大的不同在于PID的不同。fork函数被调用一次会返回两次,在父进程中,fork函数返回子进程的PID,在子进程中,fork函数返回0。
6.4 Hello的execve过程
Execve函数加载并执行可执行程序hello,并且构造参数列表和环境变量。只有当加载出现错误时函数才会返回,否则不返回。
首先execve函数会删除已经存在的用户区域,之后会为hello的代码、数据、.bss和栈区域创建新的区域结构,接着它会映射共享区,最后会设置PC,让PC指向代码的入口。
6.5 Hello的进程执行
上下文:内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
上下文切换:在内核调度了一个新的进程运行时,它就抢占当前进程,并使用一种上下文切换的机制来控制转移到新的进程。具体过程为:保存当前进程的上下文,恢复某个先前被抢占的进程被保存的上下文,将控制传递给这个新恢复的进程。
进程时间片:一个进程执行它的控制流的一部分的每一个时间段叫做时间片,多任务也叫时间分片
进程调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这种决策称为调度,是由内核中的调度器代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
用户模式和内核模式:为了保证系统安全,需要限制应用程序所能访问的地址空间范围。因而存在用户模式和内核模式的划分,内核模式拥有最高的访问权限,而用户模式的访问权限会受到一些限制。处理器使用一个寄存器作为模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,一定程度上保证了系统的安全性。
Hello在执行时是运行在用户模式下的,在运行过程中,内核不断的进行上下文的切换,使得运行过程被切分成很多的时间片,与其他进程交替进行。如果在运行时收到信号,就会进入内核模式,运行信号处理程序,之后再由信号处理程序决定下一步操作。
6.6 hello的异常与信号处理
执行截图:
图 6-1运行时随便输入
图 6-2运行时输入ctrl+c
图 6-3运行时输入ctrl+z之后输入ps和jobs指令
图 6-4进程停止时输入pstree命令(部分)
图 6-5输入kill命令
异常的分类:中断:来自I/O设备的信号(Ctrl-C、Ctrl-Z),异步。陷阱:有意的异常,执行指令的结果(例如:exit),同步。故障:潜在可恢复的错误(如:缺页异常),同步。终止:遇到不可修复的错误终止程序的运行,同步在hello运行过程中,会出现前三种异常,中断(键盘输入Ctrl-C、Ctrl-Z),陷阱(调用fork,exit函数),故障(缺页故障)。
信号:在hello运行过程中,会产生以下信号:SIGINT(键盘输入Ctrl-C),SIGSTP(键盘输入Ctrl-Z),SIGCONT(输入Ctrl-Z后使用fg命令),SIGKILL(使用kill命令),下面分别进行测试:
SIGINT:在hello运行过程中输入Ctrl-C,会给它发送SIGINT信号,终止这个进程。
图 6-6输入Ctrl-C
SIGSTP:当hello运行时,输入Ctrl-Z会向它发送SIGSTP信号,这个进程就会被挂起。
图 6-7输入Ctrl-Z
SIGCONT:当进程被挂起后,使用fg命令,会向它发送SIGCONT信号,让它继续在前台运行。
图 6-8输入fg命令
SIGKILL:当进程在后台执行时,使用kill命令可以向它发送SIGKILL信号,终止这个进程,让进程在后台执行可以在执行命令后加上 &。同时使用ps或者jobs命令查看hello进程。这两个命令不同的地方是,ps会显示出所有的进程,而jobs只会显示当前shell正在维护的进程。
图 6-9后台运行并输入ps和jobs
图 6-10输入kill命令后输入ps命令
6.7本章小结
本章阐述了hello如何运行在系统上,并且对它的异常和信号机制进行了说明。
结论
Hello程序的历程:
- hello.c源代码文件通过预处理,得到了预处理文件hello.i;
- hello.i经过编译器的编译得到汇编代码文件hello.s;
- hello.s经过汇编器的汇编得到可重定向目标文件hello.o;
- hello.o经过链接器的链接过程成为可执行目标文件hello;
- 用户在shell中键入执行hello程序的命令后,shell解释用户的输入,找到hello可执行目标文件并为其执行fork创建新进程;
- fork得到的新进程通过调用execve完成在其上下文中对hello程序的加载,hello开始执行;
- hello作为一个进程运行,接受内核的进程调度;
- hello执行的过程中,会遇到不同的异常和信号,对于不同的异常和信号会做出不同的反应;
- hello程序运行结束后,父进程shell会进行回收,内核也会清除在内存中为其创建的各种数据结构和信息。
附件
hello.i | 预处理文件 |
hello.o | 可重定位目标文件 |
hello.s | 汇编文件 |
hello_o_dis.txt | hello.o反汇编结果 |
hello_elf.txt | hello中的elf信息结果 |
hello_dis.txt | hello反汇编结果 |
hello_o_elf.txt | hello.o中的elf信息 |
hello | 可执行目标文件 |
参考文献
- GCC online documentation. GCC online documentation- GNU Project
- 深入理解计算机系统(原书第三版).机械工业出版社, 2016.
- 编译.百度百科. 编译_百度百科
- ELF文件头结构_js0huang的博客-CSDN博客_elf.h