题目:程序生活-Hello’s P2P
专业:人工智能(未来技术)
学号:7203610322
班级:2036012
学生:钟明生
导师:刘宏伟
摘要
第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
第1章 概述
1.1 Hello简介
- 由键盘输入,形成hello.c文件。
- P2P(Program to Process):hello.c经过预处理器(cpp)的预处理形成hello.i;之后由编译器(ccl)将hello.i编译成hello.s汇编程序;再由汇编器(as)将hello.s汇编成二进制的可重定位目标程序hello.o;最后由链接器(ld)将hello.o与需要的库函数printf.o链接,形成可执行目标程序hello。在shell中键入启动命令后,shell为其fork产生一个子进程,然后hello便从程序变为了进程。
- 020(Zero to Zero):在shell中键入启动命令后,shell为此创建子进程并进行execve,映射虚拟内存。进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。最终回到程序运行前的状态,就像没有hello来过一样
1.2 环境与工具
硬件环境: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
1.3 中间结果
文件名称 |
文件作用 |
---|---|
hello.c |
C语言文件 |
hello.i |
预处理后产生的文件 |
hello.s |
编译后产生的汇编文件 |
hello.o |
汇编后产生的可重定位目标文件 |
hello |
链接后产生的可执行文件 |
hello_o.txt |
hello.o通过反汇编产生的文件 |
hello.txt |
hello通过反汇编产生的文件 |
1.4 本章小结
本章大致主要简单介绍了 hello 的 P2P,020 过程,大致介绍了hello程序从c程序hello.c到可执行目标文件hello经过的历程,并列出了使用的软硬件环境,开发与调试工具,生成的中间结果文件的名称及作用。
第2章 预处理
2.1 预处理的概念与作用
- 预处理的概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
- 预处理阶段作用:
- 处理宏定义指令:预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
- 处理条件编译指令:条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
- 处理头文件包含指令:头文件包含指令如#include "FileName"或者#include 等。该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
- 处理特殊符号:预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号,FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
在 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命令还会对相应的符号进行替换。
2.4 本章小结
本章介绍了预处理的相关概念及其所进行的一些处理,例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。
第3章 编译
3.1 编译的概念与作用
- 编译的概念:编译器ccl将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。编译器以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。此外,编译器还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用等其他功能。
- 编译的作用:
- 将高级语言代码翻译成机器更加容易理解的汇编语言
- 对代码做基本的语法检查和语义检查
- 对代码进行分析优化,让其有更好的效率。
3.2 在Ubuntu下编译的命令
在 Linux Shell 中输入
gcc hello.c -s hello.s
3.3 Hello的编译结果解析
3.3.1 汇编指令的介绍
.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
- .file:声明源文件
- .text:代码节
- .section:
- .rodata:只读代码段
- .align:数据或者指令的地址对其方式
- .string:声明一个字符串(.LC0, .LC1)
- .globl:声明全局变量(main)
3.3.2 全局函数
int main(int argc, char **argv, char **envp);
在hello.c中,声明了一个全局函数main。经过编译之后,main函数中使用的字符串常量被存放在数据区。而在汇编语言中
.globl main
说明main函数是全局函数。
3.3.3 赋值操作
在C语言程序中,赋值操作如下所示:
int i = 1;
long j = 1e10;
赋值操作在汇编代码主要使用mov指令来实现。而根据数据的大小,存在四种带有不同后缀的mov指令:
指令 |
数据大小 |
---|---|
movb |
一个字节 |
movw |
两个字节 |
movl |
四个字节 |
movq |
八个字节 |
例如,下面的汇编代码:
movq %rbx, %rbp
将存放在%rbx寄存器上的八个字节的数据赋值给%rbp寄存器。
3.3.4 算数操作
在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<<k |
左移 |
SHL k,D |
D=D<<k |
左移 |
SAR k,D |
D=D>>A k |
算数右移 |
SHR k,D |
D=D>>H k |
逻辑右移 |
例如,下面的汇编代码:
subq $32, %rax
将存放在寄存器%rax上的数值减去32 。
3.3.5 关系操作
汇编语言中,关系操作对两个操作数进行操作,根据结果设置条件码。
指令 |
操作 |
描述 |
---|---|---|
CMP S1, S2 |
S2-S1 |
比较 |
TEST S1, S2 |
S1&S2 |
测试 |
3.3.6 控制转移指令
汇编语言中使用条件码,根据条件码使用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循环。
3.3.7 函数操作
调用函数时有以下操作:(假设函数P调用函数Q)
- 传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
- 传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向 P中返回一个值。3.分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
hello.c涉及的函数操作有:main函数,printf,exit,sleep,getchar函数等。
main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串,exit参数是1,sleep函数参数是atoi(argv[3])
函数的返回值存储在%eax寄存器中。
3.4 本章小结
本章主要讲述了编译阶段中编译器如何处理各种数据和操作,以及C语言中各种类型和操作所对应的的汇编代码。通过理解了这些编译器编译的机制,我们可以很容易的将汇编语言翻译成C语言。
第4章 汇编
4.1 汇编的概念与作用
- 汇编的概念:汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
- 汇编的作用:由汇编指令到机器指令,机器可以直接识别。
4.2 在Ubuntu下汇编的命令
在 Linux Shell 中输入
gcc hello.c -o hello.o
4.3 可重定位目标elf格式
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为重定向到的目标的名称。
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: 53 push %rbx
6: 48 83 ec 08 sub $0x8,%rsp
a: 83 ff 04 cmp $0x4,%edi
d: 75 0a jne 19 <main+0x19>
f: 48 89 f3 mov %rsi,%rbx
12: bd 00 00 00 00 mov $0x0,%ebp
17: eb 51 jmp 6a <main+0x6a>
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
1c: R_X86_64_PC32 .LC0-0x4
20: e8 00 00 00 00 call 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 call 2f <main+0x2f>
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 <main+0x3e>
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 <main+0x4d>
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 <main+0x60>
5c: R_X86_64_PLT32 strtol-0x4
60: 89 c7 mov %eax,%edi
62: e8 00 00 00 00 call 67 <main+0x67>
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 <main+0x2f>
6f: 48 8b 3d 00 00 00 00 mov 0x0(%rip),%rdi # 76 <main+0x76>
72: R_X86_64_PC32 stdin-0x4
76: e8 00 00 00 00 call 7b <main+0x7b>
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运行原理的语言。每一条汇编语言操作码都可以用机器二进制数据来表示,进而可以将所有的汇编语言(操作码和操作数)和二进制机器语言建立一一映射的关系,因此可以将汇编语言转化为机器语言,通过对机器代码的分析可以看出一下不同的地方。
- 分支转移:反汇编的跳转指令用的不是段名称比如.L3,二是用的确定的地址,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。
- 函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程 序中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数 都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执 行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。
4.5 本章小结
本章对hello.s进行了汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处,分析了从汇编语言到机器语言的一一映射关系。
第5章 链接
5.1 链接的概念与作用
- 链接的概念:链接是链接器(ld)将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。
- 链接的作用
- 模块化:一个程序可以分成很多源程序文件;可构建公共函数库,如数学库,标准C库等。以便代码重用,提高开发效率。
- 效率高:时间上,可分开编译。只需要重新编译修改的源程序文件,然后重新链接;空间上,无需包含共享库所有代码:源文件中无需包含共享库函数的源码,只要直接调用即可(如,只要直接调用printf() 函数,无需包含其源码)。另外,可执行文件和运行时的内存中只需包含所调用函数的代码,而不需要包含整个共享库。
5.2 在Ubuntu下链接的命令
使用命令
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
生成可执行文件。
5.3 可执行目标文件hello的格式
1. ELF头:
2. 节头部表
Section Headers 对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
3. 重定位节.rela.text:
4. 符号表.symtab
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息。