题目:程序人生-Hello’s P2P
专业:人工智能(未来技术)
学号:7203610322
班级:2036012
学生:钟洺生
指导教师:刘宏伟
计算机科学与技术学院
2021年5月
摘要
第1章 概述
1.1 Hello简介
1.2 环境与工具
1.3 中间结果
1.4 本章小结
第2章 预处理
2.1 预处理的概念与作用
2.2在Ubuntu下预处理的命令
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 汇编指令的介绍
3.3.2 全局函数
3.3.3 赋值操作
3.3.4 算数操作
3.3.5 关系操作
3.3.6 控制转移指令
3.3.7 函数操作
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.4 Hello.o的结果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 hello进程管理
6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处理流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程执行
6.6 hello的异常与信号处理
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处理
7.9 动态存储分配管理
7.10本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5 本章小结
结论
附件
参考文献
本文以hello程序为切入点,详细阐述了程序由源代码hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程。同时介绍了让程序得以正确运行的进程管理、存储管理、IO管理等系统机制。通过对hello一生周期的探索,我们可以对计算机系统有更深入的了解。
关键词:Hello程序;预处理;编译;汇编;链接;进程;存储;虚拟内存;I/O ;shell;Cache;页表;TLB
硬件环境:AMD Ryzen 7 4800H with Radeon Graphics 2.90 GHz; 16.0 GB RAM
软件环境:Windows 10 64位;Ubuntu 22.04 LTS 64位
开发工具:visual studio code, vim, gcc, objdump, gdb, readelf
文件名称 |
文件作用 |
---|---|
hello.c |
C语言文件 |
hello.i |
预处理后产生的文件 |
hello.s |
编译后产生的汇编文件 |
hello.o |
汇编后产生的可重定位目标文件 |
hello |
链接后产生的可执行文件 |
hello_o.txt |
hello.o通过反汇编产生的文件 |
hello.txt |
hello通过反汇编产生的文件 |
本章大致主要简单介绍了 hello 的 P2P,020 过程,大致介绍了hello程序从c程序hello.c到可执行目标文件hello经过的历程,并列出了使用的软硬件环境,开发与调试工具,生成的中间结果文件的名称及作用。
在 Linux Shell 中输入
gcc hello.c -E -o hello.i
预处理后的hello.i 文件部分代码如下:
...
extern int rpmatch (const char *__response) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1))) ;
# 967 "/usr/include/stdlib.h" 3 4
extern int getsubopt (char **__restrict __optionp,
char *const *__restrict __tokens,
char **__restrict __valuep)
__attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1, 2, 3))) ;
# 1013 "/usr/include/stdlib.h" 3 4
extern int getloadavg (double __loadavg[], int __nelem)
__attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1)));
# 1023 "/usr/include/stdlib.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/stdlib-float.h" 1 3 4
# 1024 "/usr/include/stdlib.h" 2 3 4
# 1035 "/usr/include/stdlib.h" 3 4
# 9 "hello.c" 2
# 10 "hello.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;
}
经过预处理之后,hello.c转化为hello.i文件,打开该文件可以发现,文件的内容增加,且仍为可以阅读的C语言程序文本文件。对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。
本章介绍了预处理的相关概念及其所进行的一些处理,例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。
在 Linux Shell 中输入
gcc hello.c -s hello.s
.file "hello.c"
.text
.section .rodata
.align 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"
.text
.globl main
.type main, @function
int main(int argc, char **argv, char **envp);
在hello.c中,声明了一个全局函数main。经过编译之后,main函数中使用的字符串常量被存放在数据区。而在汇编语言中
.globl main
说明main函数是全局函数。
在C语言程序中,赋值操作如下所示:
int i = 1;
long j = 1e10;
赋值操作在汇编代码主要使用mov指令来实现。而根据数据的大小,存在四种带有不同后缀的mov指令:
指令 |
数据大小 |
---|---|
movb |
一个字节 |
movw |
两个字节 |
movl |
四个字节 |
movq |
八个字节 |
例如,下面的汇编代码:
movq %rbx, %rbp
将存放在%rbx寄存器上的八个字节的数据赋值给%rbp寄存器。
在C语言程序中,赋值操作如下所示:
int i, j;
i = j + 1;
i = j - 1;
i = j * 2;
i = j / 2;
i++;
i--;
i = i >> 2;
I = i << 2;
算数操作在汇编代码的实现方式如下:
指令 |
效果 |
描述 |
---|---|---|
leaq S,D |
D=&S |
加载有效地址 |
INC D |
D+=1 |
加1 |
DEC D |
D-=1 |
减1 |
NEG D |
D=-D |
取负 |
NOT D |
D=~D |
取补 |
ADD S,D |
D+=S |
加 |
SUB S,D |
D-=S |
减 |
IMUL S,D |
D*=S |
乘 |
XOR S,D |
D^=S |
异或 |
OR S,D |
D|=S |
或 |
AND S,D |
D&=S |
与 |
SAL k,D |
D=D< |
左移 |
SHL k,D |
D=D< |
左移 |
SAR k,D |
D=D>>A k |
算数右移 |
SHR k,D |
D=D>>H k |
逻辑右移 |
例如,下面的汇编代码:
subq $32, %rax
将存放在寄存器%rax上的数值减去32 。
汇编语言中,关系操作对两个操作数进行操作,根据结果设置条件码。
指令 |
操作 |
描述 |
---|---|---|
CMP S1, S2 |
S2-S1 |
比较 |
TEST S1, S2 |
S1&S2 |
测试 |
汇编语言中使用条件码,根据条件码使用jmp语句进行控制转移。C语言中的if语句,while语句以及for语句都可以用汇编语言的控制转移指令完成。
1. hello.c中的if语句:
if(argc!=4){
printf("用法: Hello 学号 姓名 秒数!\n");
exit(1);
}
在汇编中对应代码如下:
cmpl $4, %edi
jne .L6
...
.L6:
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
argc存放在%edi中,cmp指令比较4和%edi的值,若不相同则跳转到.L6,设置参数值并调用puts函数,之后设置返回值并调用exit函数退出。
2. hello.c中的for语句:
for(i=0;i<8;i++){
printf("Hello %s %s\n",argv[1],argv[2]);
sleep(atoi(argv[3]));
}
在汇编中对应代码如下:
movq %rsi, %rbx
movl $0, %ebp
jmp .L2
.L3:
movq 16(%rbx), %rcx
movq 8(%rbx), %rdx
leaq .LC1(%rip), %rsi
movl $1, %edi
movl $0, %eax
call __printf_chk@PLT
movq 24(%rbx), %rdi
movl $10, %edx
movl $0, %esi
call strtol@PLT
movl %eax, %edi
call sleep@PLT
addl $1, %ebp
.L2:
cmpl $7, %ebp
jle .L3
movq stdin(%rip), %rdi
call getc@PLT
movl $0, %eax
addq $8, %rsp
.cfi_def_cfa_offset 24
popq %rbx
.cfi_def_cfa_offset 16
popq %rbp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
程序将0存放在%ebp中然后跳转到.L2,.L3比较7和%ebp的大小,如果%ebp≤7则跳转到.L3,调用printf函数,sleep函数等,最后把%ebp的值加1。之后程序进入.L2,相当于重新开始for循环。
调用函数时有以下操作:(假设函数P调用函数Q)
hello.c涉及的函数操作有:main函数,printf,exit,sleep,getchar函数等。
main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串,exit参数是1,sleep函数参数是atoi(argv[3])
函数的返回值存储在%eax寄存器中。
本章主要讲述了编译阶段中编译器如何处理各种数据和操作,以及C语言中各种类型和操作所对应的的汇编代码。通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成C语言。
在 Linux Shell 中输入
gcc hello.c -o hello.o
1. ELF Header(ELF头):使用命令
readelf -h hello.o
得到ELF Header如下:
ELF头展示了机器和文件的最基本信息。
2. Section Header(ELF节头部表):使用命令
readelf -S hello.o
得到Section Header如下:
节头部表包含了文件中出现的各个节的语义,包括节 的类型、位置和大小等信息。 由于是可重定位目标文件,所以每个节都从0开始,用于重定位。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,同时可以观察到,代码是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
3. .symtab(符号表):使用命令
readelf -s hello.o
得到.symtab表如下(部分):
.symtab表存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。
4. .rela.text(重定位节):使用命令
readelf -r hello.o
得到.rela.text如下:
重定位节是一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息当链接器把这个目标文件和其他文件组合时,需要修改这些位置。重定位节.rela.text中,Offset表示需要被修改的引用节的偏移;Info包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节。symbol表示标识被修改引用应该指向的符号,type表示重定位的类型。Type告知链接器应该如何修改新的应用;Attend为一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整;Name为重定向到的目标的名称。
使用指令
objdump -d -r hello.o
得到反汇编代码如下:
hello.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 :
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 53 push %rbx
6: 48 83 ec 08 sub $0x8,%rsp
a: 83 ff 04 cmp $0x4,%edi
d: 75 0a jne 19
f: 48 89 f3 mov %rsi,%rbx
12: bd 00 00 00 00 mov $0x0,%ebp
17: eb 51 jmp 6a
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20
1c: R_X86_64_PC32 .LC0-0x4
20: e8 00 00 00 00 call 25
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi
2a: e8 00 00 00 00 call 2f
2b: R_X86_64_PLT32 exit-0x4
2f: 48 8b 4b 10 mov 0x10(%rbx),%rcx
33: 48 8b 53 08 mov 0x8(%rbx),%rdx
37: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 3e
3a: R_X86_64_PC32 .LC1-0x4
3e: bf 01 00 00 00 mov $0x1,%edi
43: b8 00 00 00 00 mov $0x0,%eax
48: e8 00 00 00 00 call 4d
49: R_X86_64_PLT32 __printf_chk-0x4
4d: 48 8b 7b 18 mov 0x18(%rbx),%rdi
51: ba 0a 00 00 00 mov $0xa,%edx
56: be 00 00 00 00 mov $0x0,%esi
5b: e8 00 00 00 00 call 60
5c: R_X86_64_PLT32 strtol-0x4
60: 89 c7 mov %eax,%edi
62: e8 00 00 00 00 call 67
63: R_X86_64_PLT32 sleep-0x4
67: 83 c5 01 add $0x1,%ebp
6a: 83 fd 07 cmp $0x7,%ebp
6d: 7e c0 jle 2f
6f: 48 8b 3d 00 00 00 00 mov 0x0(%rip),%rdi # 76
72: R_X86_64_PC32 stdin-0x4
76: e8 00 00 00 00 call 7b
77: R_X86_64_PLT32 getc-0x4
7b: b8 00 00 00 00 mov $0x0,%eax
80: 48 83 c4 08 add $0x8,%rsp
84: 5b pop %rbx
85: 5d pop %rbp
86: c3 ret
汇编语言的main部分如下:
main:
.LFB51:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
pushq %rbx
.cfi_def_cfa_offset 24
.cfi_offset 3, -24
subq $8, %rsp
.cfi_def_cfa_offset 32
cmpl $4, %edi
jne .L6
movq %rsi, %rbx
movl $0, %ebp
jmp .L2
.L6:
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
.L3:
movq 16(%rbx), %rcx
movq 8(%rbx), %rdx
leaq .LC1(%rip), %rsi
movl $1, %edi
movl $0, %eax
call __printf_chk@PLT
movq 24(%rbx), %rdi
movl $10, %edx
movl $0, %esi
call strtol@PLT
movl %eax, %edi
call sleep@PLT
addl $1, %ebp
.L2:
cmpl $7, %ebp
jle .L3
movq stdin(%rip), %rdi
call getc@PLT
movl $0, %eax
addq $8, %rsp
.cfi_def_cfa_offset 24
popq %rbx
.cfi_def_cfa_offset 16
popq %rbp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
通过反汇编的代码和hello.s进行比较,发现汇编语言的指令并没有什么不同的地方,只是反汇编代码所显示的不仅仅是汇编代码,还有机器代码,机器语言程序的是二进制机器指令的集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数构成,汇编语言是人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。
使用命令
ld -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 /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello -lpthread
生成可执行文件。
1. ELF头:
2. 节头部表
Section Headers 对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
3. 重定位节.rela.text:
4. 符号表.symtab
使用edb加载hello,查看本进程的虚拟地址空间各段信息。
1. .init段
2. .text段
可以通过edb找到各个节的信息,比如通过查看edb,看出hello的虚拟地址空间开始于0x400000,结束与0x400ff0。比如查看hello的.txt节,虚拟地址开始于0x400550,大小为0x132。
在控制台输入反汇编命令objdump -d -r hello,生成反汇编代码:
hello: 文件格式 elf64-x86-64
Disassembly of section .init:
0000000000001000 <_init>:
1000: f3 0f 1e fa endbr64
1004: 48 83 ec 08 sub $0x8,%rsp
1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__@Base>
100f: 48 85 c0 test %rax,%rax
1012: 74 02 je 1016 <_init+0x16>
1014: ff d0 call *%rax
1016: 48 83 c4 08 add $0x8,%rsp
101a: c3 ret
Disassembly of section .plt:
0000000000001020 <.plt>:
1020: ff 35 72 2f 00 00 push 0x2f72(%rip) # 3f98 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: f2 ff 25 73 2f 00 00 bnd jmp *0x2f73(%rip) # 3fa0 <_GLOBAL_OFFSET_TABLE_+0x10>
102d: 0f 1f 00 nopl (%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 push $0x0
1039: f2 e9 e1 ff ff ff bnd jmp 1020 <_init+0x20>
103f: 90 nop
1040: f3 0f 1e fa endbr64
1044: 68 01 00 00 00 push $0x1
1049: f2 e9 d1 ff ff ff bnd jmp 1020 <_init+0x20>
104f: 90 nop
1050: f3 0f 1e fa endbr64
1054: 68 02 00 00 00 push $0x2
1059: f2 e9 c1 ff ff ff bnd jmp 1020 <_init+0x20>
105f: 90 nop
1060: f3 0f 1e fa endbr64
1064: 68 03 00 00 00 push $0x3
1069: f2 e9 b1 ff ff ff bnd jmp 1020 <_init+0x20>
106f: 90 nop
1070: f3 0f 1e fa endbr64
1074: 68 04 00 00 00 push $0x4
1079: f2 e9 a1 ff ff ff bnd jmp 1020 <_init+0x20>
107f: 90 nop
1080: f3 0f 1e fa endbr64
1084: 68 05 00 00 00 push $0x5
1089: f2 e9 91 ff ff ff bnd jmp 1020 <_init+0x20>
108f: 90 nop
Disassembly of section .plt.got:
0000000000001090 <__cxa_finalize@plt>:
1090: f3 0f 1e fa endbr64
1094: f2 ff 25 5d 2f 00 00 bnd jmp *0x2f5d(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .plt.sec:
00000000000010a0 :
10a0: f3 0f 1e fa endbr64
10a4: f2 ff 25 fd 2e 00 00 bnd jmp *0x2efd(%rip) # 3fa8
10ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010b0 :
10b0: f3 0f 1e fa endbr64
10b4: f2 ff 25 f5 2e 00 00 bnd jmp *0x2ef5(%rip) # 3fb0
10bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010c0 :
10c0: f3 0f 1e fa endbr64
10c4: f2 ff 25 ed 2e 00 00 bnd jmp *0x2eed(%rip) # 3fb8
10cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010d0 :
10d0: f3 0f 1e fa endbr64
10d4: f2 ff 25 e5 2e 00 00 bnd jmp *0x2ee5(%rip) # 3fc0
10db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010e0 :
10e0: f3 0f 1e fa endbr64
10e4: f2 ff 25 dd 2e 00 00 bnd jmp *0x2edd(%rip) # 3fc8
10eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010f0 :
10f0: f3 0f 1e fa endbr64
10f4: f2 ff 25 d5 2e 00 00 bnd jmp *0x2ed5(%rip) # 3fd0
10fb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .text:
0000000000001100 <_start>:
1100: f3 0f 1e fa endbr64
1104: 31 ed xor %ebp,%ebp
1106: 49 89 d1 mov %rdx,%r9
1109: 5e pop %rsi
110a: 48 89 e2 mov %rsp,%rdx
110d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1111: 50 push %rax
1112: 54 push %rsp
1113: 45 31 c0 xor %r8d,%r8d
1116: 31 c9 xor %ecx,%ecx
1118: 48 8d 3d ca 00 00 00 lea 0xca(%rip),%rdi # 11e9
111f: ff 15 b3 2e 00 00 call *0x2eb3(%rip) # 3fd8 <__libc_start_main@GLIBC_2.34>
1125: f4 hlt
1126: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
112d: 00 00 00
0000000000001130 :
1130: 48 8d 3d d9 2e 00 00 lea 0x2ed9(%rip),%rdi # 4010 <__TMC_END__>
1137: 48 8d 05 d2 2e 00 00 lea 0x2ed2(%rip),%rax # 4010 <__TMC_END__>
113e: 48 39 f8 cmp %rdi,%rax
1141: 74 15 je 1158
1143: 48 8b 05 96 2e 00 00 mov 0x2e96(%rip),%rax # 3fe0 <_ITM_deregisterTMCloneTable@Base>
114a: 48 85 c0 test %rax,%rax
114d: 74 09 je 1158
114f: ff e0 jmp *%rax
1151: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
1158: c3 ret
1159: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
0000000000001160 :
1160: 48 8d 3d a9 2e 00 00 lea 0x2ea9(%rip),%rdi # 4010 <__TMC_END__>
1167: 48 8d 35 a2 2e 00 00 lea 0x2ea2(%rip),%rsi # 4010 <__TMC_END__>
116e: 48 29 fe sub %rdi,%rsi
1171: 48 89 f0 mov %rsi,%rax
1174: 48 c1 ee 3f shr $0x3f,%rsi
1178: 48 c1 f8 03 sar $0x3,%rax
117c: 48 01 c6 add %rax,%rsi
117f: 48 d1 fe sar %rsi
1182: 74 14 je 1198
1184: 48 8b 05 65 2e 00 00 mov 0x2e65(%rip),%rax # 3ff0 <_ITM_registerTMCloneTable@Base>
118b: 48 85 c0 test %rax,%rax
118e: 74 08 je 1198
1190: ff e0 jmp *%rax
1192: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
1198: c3 ret
1199: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000000011a0 <__do_global_dtors_aux>:
11a0: f3 0f 1e fa endbr64
11a4: 80 3d 65 2e 00 00 00 cmpb $0x0,0x2e65(%rip) # 4010 <__TMC_END__>
11ab: 75 2b jne 11d8 <__do_global_dtors_aux+0x38>
11ad: 55 push %rbp
11ae: 48 83 3d 42 2e 00 00 cmpq $0x0,0x2e42(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
11b5: 00
11b6: 48 89 e5 mov %rsp,%rbp
11b9: 74 0c je 11c7 <__do_global_dtors_aux+0x27>
11bb: 48 8b 3d 46 2e 00 00 mov 0x2e46(%rip),%rdi # 4008 <__dso_handle>
11c2: e8 c9 fe ff ff call 1090 <__cxa_finalize@plt>
11c7: e8 64 ff ff ff call 1130
11cc: c6 05 3d 2e 00 00 01 movb $0x1,0x2e3d(%rip) # 4010 <__TMC_END__>
11d3: 5d pop %rbp
11d4: c3 ret
11d5: 0f 1f 00 nopl (%rax)
11d8: c3 ret
11d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000000011e0 :
11e0: f3 0f 1e fa endbr64
11e4: e9 77 ff ff ff jmp 1160
00000000000011e9 :
11e9: f3 0f 1e fa endbr64
11ed: 55 push %rbp
11ee: 48 89 e5 mov %rsp,%rbp
11f1: 48 83 ec 20 sub $0x20,%rsp
11f5: 89 7d ec mov %edi,-0x14(%rbp)
11f8: 48 89 75 e0 mov %rsi,-0x20(%rbp)
11fc: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
1200: 74 19 je 121b
1202: 48 8d 05 ff 0d 00 00 lea 0xdff(%rip),%rax # 2008 <_IO_stdin_used+0x8>
1209: 48 89 c7 mov %rax,%rdi
120c: e8 8f fe ff ff call 10a0
1211: bf 01 00 00 00 mov $0x1,%edi
1216: e8 c5 fe ff ff call 10e0
121b: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
1222: eb 4b jmp 126f
1224: 48 8b 45 e0 mov -0x20(%rbp),%rax
1228: 48 83 c0 10 add $0x10,%rax
122c: 48 8b 10 mov (%rax),%rdx
122f: 48 8b 45 e0 mov -0x20(%rbp),%rax
1233: 48 83 c0 08 add $0x8,%rax
1237: 48 8b 00 mov (%rax),%rax
123a: 48 89 c6 mov %rax,%rsi
123d: 48 8d 05 ea 0d 00 00 lea 0xdea(%rip),%rax # 202e <_IO_stdin_used+0x2e>
1244: 48 89 c7 mov %rax,%rdi
1247: b8 00 00 00 00 mov $0x0,%eax
124c: e8 5f fe ff ff call 10b0
1251: 48 8b 45 e0 mov -0x20(%rbp),%rax
1255: 48 83 c0 18 add $0x18,%rax
1259: 48 8b 00 mov (%rax),%rax
125c: 48 89 c7 mov %rax,%rdi
125f: e8 6c fe ff ff call 10d0
1264: 89 c7 mov %eax,%edi
1266: e8 85 fe ff ff call 10f0
126b: 83 45 fc 01 addl $0x1,-0x4(%rbp)
126f: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
1273: 7e af jle 1224
1275: e8 46 fe ff ff call 10c0
127a: b8 00 00 00 00 mov $0x0,%eax
127f: c9 leave
1280: c3 ret
Disassembly of section .fini:
0000000000001284 <_fini>:
1284: f3 0f 1e fa endbr64
1288: 48 83 ec 08 sub $0x8,%rsp
128c: 48 83 c4 08 add $0x8,%rsp
1290: c3 ret
结合hello.o的重定位项目,分析hello重定位的方式如下:
hello各函数地址如下表:
地址 |
函数名 |
---|---|
0x53216100 |
hello!_start |
→0x532161e9 |
main |
→0x53216030 |
hello!puts@plt |
→0x53216070 |
hello!exit@plt |
→0x53216040 |
hello!printf@plt |
→0x53216080 |
hello!getchar@plt |
→0x53216080 |
hello!sleep@plt |
在对hello的readelf分析中得知,.got表的地址为0x0000000000003f90,通过edb中对Data Dump窗口跳转,定位到GOT表处。
1. 调用_init之前的GOT表
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
2. 调用_init后的GOT表
执行了dl_init后的global offset表内的数据都发生了变化。动态链接器使用过程链接表PLT + 全局变量偏移表GOT实现函数的动态链接,GOT中存放目标函数的地址,PLT使用该地址跳转到目标位置,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。
在本章中主要介绍了链接的概念与作用,并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
当在终端中输入./hello 学号 姓名时。shell会通过上述流程处理,首先判断出它不是内置命令,所以会认为它是一个当前目录下的可执行文件hello。在加载此进程时shell通过fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库和用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程有不同的pid。父进程和子进程是并发运行的独立进程,内核可以任意方式交替执行他们的逻辑控制流中的指令,所以这会导致我们不能简单的凭直觉判断指令执行的顺序。父进程会默认等待子进程执行完之后回收子进程,但是也会有产生僵死进程的情况,父进程可以调用waitpid函数等待其子进程终止或停止
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
观察hello的运行,当运行到sleep,shell会并发的去执行另一个进程,如图所示,在上下文切换的时候,会进入内核模式,且有以下动作:
1. 异常
在发生异常时会发出信号,比如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。
hello程序可能发生的异常有:1) 缺页异常; 2)来自键盘的中断;3)陷阱进入系统函数sleep;
2. 常见信号及其对应功能:
3. hello程序运行时接收信号的情况
1)hello正确执行的情况:
2)胡乱按键盘:对进程没有影响
3)回车:
4)Ctrl+Z:发送SIGTSTP信号停止hello进程,程序被挂起,收到信号SIGCONT之后会继续执行。
i)此时调用ps命令,发现进程上还有hello程序,其状态为T(停止)
ii)调用jobs命令:获取bash上作业
iii)调用pstree命令:获取进程树
可以找到systemd-systemd-gnome-terminal-bash-hello这一进程树分支。
iv)调用fg命令:恢复前台进程
v)调用kill命令:传送一个信号到指定进程
5)Ctrl+C发送SIGINT到指定进程,默认情况是终止该进程
此时调用ps,发现没有hello这一进程。
在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用execve函数执行hello,hello的进程执行,以及hello的异常与信号处理。
逻辑地址由段地址和偏移地址组成。计算机中共有4个段寄存器,用于存放数据、代码、堆栈、辅助4段的基地址,段选择符共计16位,前13位为索引位,用于确定段描述符在描述符表中的位置。第14位为Tl位,Tl=0时选择全局描述符表,Tl=1时选择局部描述符表。最后两位用于描述段的状态被选中的描述符先被送至描述符Cache,每次从描述符Cache中取32位基地址,与32位段内偏移量(有效地址)相加得到线性地址
线性地址(也就是虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。不考虑TLB和多级页表,使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的住哪用硬件,利用主存中的查询表来动态翻译虚拟地址。
虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小是相同的。
任意时刻虚拟页都被分为三个不相交的子集:
每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页的起始地址。
线性地址的后12位是页内偏移(VPO)。前面36位是虚拟页号(VPN),通过VPN可以找到相应的物理地址所在的页,如果有多级页表,VPN将会被分成多份。第i个VPN作为第i级页表的索引指向第i+1级页表的基址。最后一级页表中的PTE包含每个物理页面的页号(PPN)。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
在Intel Core i7环境下研究 VA 到 PA 的地址翻译问题。前提如下:虚拟地址空间 48 位,物理地址空间 52 位,页表大小4KB,4级页表。TLB 4路16组相联。CR3 指向第一级页表的起始位置(上下文一部分)。解析前提条件:由一个页表大小4KB,一个PTE条目8B,共512个条目,使用9位二进制索引,一共4个页表共使用36位二进制索引,所以VPN共36位,因为VA 48位,所以VPO 12位;因为 TLB 共16组,所以TLBI需4位,因为VPN 36位,所以TLBT 32位。
CPU 产生虚拟地址VA,VA 传送给MMU,MMU使用前36位 VPN作为 TLB(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与 VPO 组合成 PA,并且向TLB中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
以Core i7内存系统为例,处理器封装包括4个核,一个大的所有核共享的L3高速缓存;一个DD3的内存控制器。灭个和包括一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点链路。
得到物理地址之后,先将物理地址拆分成CT(标记)+CI(索引)+CO(偏移量),然后在一级cache内部找,如果未能寻找到标记位为有效的字节(miss)的话就去二级和三级cache中寻找对应的字节,找到之后返回结果。
Linux通过将虚拟内存区域与磁盘上的对象关联起来以初始化这个虚拟内存区域的内容. 这个过程称为内存映射。
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。
为新进程创建虚拟内存:
加载并运行hello需要以下几个步骤:
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。故障处理具体流程如图7.8所示。
动态储存分配管理使用动态内存分配器(如malloc)来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合。每个块就是一个连续的虚拟内存页,要么是已分配的,要么是空闲的。
已分配的块显式地保留为供应用程序使用。空闲块保持空闲,直到它显式地被应用所分配。
一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有两种基本方法与策略:
1. 带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、(可能的)额外填充以及一个字的尾部组成。
隐式空闲链表:空闲块通过头部的大小字段隐含地连接着。分配器遍历堆中所有的块,间接地遍历整个空闲块的集合。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配和最佳适配。分配器在面对释放一个已分配块时,可以合并相邻的空闲块,其中一种简单的方式,是利用隐式空闲链表的边界标记来进行合并。
2. 显式空间链表管理
显式空闲链表是将堆的空闲块组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。进行内存管理。在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
本章讨论了存储器地址空间,段式管理、页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理。
一个Linux文件就是一个m字节的序列:B0,B1,B2……Bm
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:
设备的模型化:文件
设备管理:unix io接口
Unix I/O 接口:
Unix I/O 函数:
1. open函数
int open(char* filename,int flags,mode_t mode);
进程通过调用 open 函数来打开一个存在的文件或是创建一个新文件的。open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2. close函数
int close(fd);
fd是需要关闭的文件的描述符,close返回操作结果。
3. read函数
ssize_t read(int fd,void *buf,size_t n);
read 函数从描述符为fd的当前文件位置赋值最多 n个字节到内存位置buf。返回值-1 表示一个错误,0 表示EOF,否则返回值表示的是实际传送的字节数量。
4. write函数
ssize_t wirte(int fd,const void *buf,size_t n);
write函数从内存位置 buf复制至多n个字节到描述符为 fd 的当前文件位置。
1. printf函数如下:
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1, printbuf, i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
由此可见,printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
2. write 函数
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
3. sys_call 函数
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上。
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb = buf;
static int n = 0;
if (n==0) {
n = read(0, buf, BUFSIZ);
bb = buf;
}
return (--n>=0)?(unsigned char)* bb++ : EOF;
}
getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
本章主要阐述了Linux的IO设备管理办法以及IO接口实现与相应的函数实现。分析了getchar()和printf()函数的实现。
用计算机系统的语言,逐条总结hello所经历的过程。
对计算机系统的设计与实现的感悟
计算机系统的设计思想和实现都是基于抽象实现的。从最底层的信息的表示用二进制表示抽象开始,到实现操作系统管理硬件的抽象:进程是对处理器、主存和I/O设备的抽象。虚拟内存是对主存和磁盘设备的抽象。文件是对I/O设备的抽象。计算机系统的设计精巧:为了解决快的设备存储小、存储大的设备慢的不平衡,设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度。计算机系统的设计考虑也十分全面:计算机系统设计考虑一切可能的实际情况,设计出一系列的满足不同情况的策略。比如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。
通过对hello程序的分析,成功地回忆和加深了对计算机系统所学的知识的理解。更重要的是,能够通过hello的“一生”将各个章节的知识联系起来,让我们对计算机系统有了一个全面的认识。同时,我们也学会了针对计算机系统编写出更对计算机友好的,性能更好的程序。
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] Computer System: A Programmer’s Perspective, Third Edition
[8] Linux文件IO操作函数概述(https://www.cnblogs.com/wangkeqin/p/9226825.html)
[9] printf函数实现的深入剖析(https://www.cnblogs.com/pianist/p/3315801.html)