题目:程序人生-Hello’s P2P
专业:人工智能(未来技术)
学号:7203610322
班级:2036012
学生:钟洺生
指导教师:刘宏伟
计算机科学与技术学院
2021年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,查看本进程的虚拟地址空间各段信息。
1. .init段
2. .text段
可以通过edb找到各个节的信息,比如通过查看edb,看出hello的虚拟地址空间开始于0x400000,结束与0x400ff0。比如查看hello的.txt节,虚拟地址开始于0x400550,大小为0x132。
5.5 链接的重定位过程分析
在控制台输入反汇编命令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 <puts@plt>:
10a0: f3 0f 1e fa endbr64
10a4: f2 ff 25 fd 2e 00 00 bnd jmp *0x2efd(%rip) # 3fa8 <puts@GLIBC_2.2.5>
10ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010b0 <printf@plt>:
10b0: f3 0f 1e fa endbr64
10b4: f2 ff 25 f5 2e 00 00 bnd jmp *0x2ef5(%rip) # 3fb0 <printf@GLIBC_2.2.5>
10bb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010c0 <getchar@plt>:
10c0: f3 0f 1e fa endbr64
10c4: f2 ff 25 ed 2e 00 00 bnd jmp *0x2eed(%rip) # 3fb8 <getchar@GLIBC_2.2.5>
10cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010d0 <atoi@plt>:
10d0: f3 0f 1e fa endbr64
10d4: f2 ff 25 e5 2e 00 00 bnd jmp *0x2ee5(%rip) # 3fc0 <atoi@GLIBC_2.2.5>
10db: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010e0 <exit@plt>:
10e0: f3 0f 1e fa endbr64
10e4: f2 ff 25 dd 2e 00 00 bnd jmp *0x2edd(%rip) # 3fc8 <exit@GLIBC_2.2.5>
10eb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000000010f0 <sleep@plt>:
10f0: f3 0f 1e fa endbr64
10f4: f2 ff 25 d5 2e 00 00 bnd jmp *0x2ed5(%rip) # 3fd0 <sleep@GLIBC_2.2.5>
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 <main>
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 <deregister_tm_clones>:
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 <deregister_tm_clones+0x28>
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 <deregister_tm_clones+0x28>
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 <register_tm_clones>:
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 <register_tm_clones+0x38>
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 <register_tm_clones+0x38>
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 <deregister_tm_clones>
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 <frame_dummy>:
11e0: f3 0f 1e fa endbr64
11e4: e9 77 ff ff ff jmp 1160 <register_tm_clones>
00000000000011e9 <main>:
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 <main+0x32>
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 <puts@plt>
1211: bf 01 00 00 00 mov $0x1,%edi
1216: e8 c5 fe ff ff call 10e0 <exit@plt>
121b: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
1222: eb 4b jmp 126f <main+0x86>
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 <printf@plt>
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 <atoi@plt>
1264: 89 c7 mov %eax,%edi
1266: e8 85 fe ff ff call 10f0 <sleep@plt>
126b: 83 45 fc 01 addl $0x1,-0x4(%rbp)
126f: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
1273: 7e af jle 1224 <main+0x3b>
1275: e8 46 fe ff ff call 10c0 <getchar@plt>
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中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
- 增加一些节:hello中增加了.init和.plt节,和一些节中定义的函数。
- 函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
- 地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
5.6 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 |
5.7 Hello的动态链接分析
在对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运行时地址。
5.8 本章小结
在本章中主要介绍了链接的概念与作用,并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 进程的作用:进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。可以说,如果没有进程,体系如此庞大的计算机不可能设计出来。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
- 终端进程读取用户由键盘输入的命令行
- 分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
- 检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
- 如果不是内部命令,调用fork( )创建新进程/子进程
- 在子进程中,用步骤2获取的参数,调用execve( )执行指定程序
- 如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…等待作业终止后返回
- 如果用户要求后台运行(如果命令末尾有&号),则shell返回
6.3 Hello的fork进程创建过程
当在终端中输入./hello 学号 姓名时。shell会通过上述流程处理,首先判断出它不是内置命令,所以会认为它是一个当前目录下的可执行文件hello。在加载此进程时shell通过fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库和用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程与父进程有不同的pid。父进程和子进程是并发运行的独立进程,内核可以任意方式交替执行他们的逻辑控制流中的指令,所以这会导致我们不能简单的凭直觉判断指令执行的顺序。父进程会默认等待子进程执行完之后回收子进程,但是也会有产生僵死进程的情况,父进程可以调用waitpid函数等待其子进程终止或停止
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行需要以下几个步骤:
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
- 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。如图6.4
- 映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
- 设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
- 用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
- 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
- 逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
- 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
观察hello的运行,当运行到sleep,shell会并发的去执行另一个进程,如图所示,在上下文切换的时候,会进入内核模式,且有以下动作:
- 保存A进程的上下文
- 恢复B进程被保存的上下文
- 将控制传递给这个B进程,来完成上下文切换
6.6 hello的异常与信号处理
1. 异常
- 中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
- 陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
- 故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
- 终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
在发生异常时会发出信号,比如缺页故障会导致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这一进程。
6.7本章小结
在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用execve函数执行hello,hello的进程执行,以及hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
-
- 逻辑地址:在机器语言中用来确定一个指令或者是操作数的地址。逻辑地址包含段和偏移量,而偏移量是相对偏移,而段则确定了偏移开始的地方,这样就能通过段和偏移来确定地址。在hello中指hello.o中的相对偏移地址。
- 线性地址:逻辑地址与物理地址之间的桥梁。用偏移加上段的地址就能得到线性地址,也就是虚拟内存地址。在hello中指hello中的虚拟内存地址
- 虚拟地址:虚拟地址和线性地址一样。
- 物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。hello的虚拟地址通过地址翻译器可转换成物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段地址和偏移地址组成。计算机中共有4个段寄存器,用于存放数据、代码、堆栈、辅助4段的基地址,段选择符共计16位,前13位为索引位,用于确定段描述符在描述符表中的位置。第14位为Tl位,Tl=0时选择全局描述符表,Tl=1时选择局部描述符表。最后两位用于描述段的状态被选中的描述符先被送至描述符Cache,每次从描述符Cache中取32位基地址,与32位段内偏移量(有效地址)相加得到线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(也就是虚拟地址 VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。不考虑TLB和多级页表,使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前首先转换为适当的物理地址。将一个虚拟地址转换为物理地址叫做地址翻译,需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(MMU)的住哪用硬件,利用主存中的查询表来动态翻译虚拟地址。
虚拟地址作为到磁盘上存放字节的数组的索引,磁盘上的数组内容被缓存在主存中。同时,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传送单元。虚拟内存分割被成为虚拟页。物理内存被分割为物理页,物理页和虚拟页的大小是相同的。
任意时刻虚拟页都被分为三个不相交的子集:
- 未分配的:VM系统还未分配的页
- 缓存的:当前已经缓存在物理内存的已分配页
- 未缓存的:当前未缓存在物理内存的已分配页
每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页的起始地址。
线性地址的后12位是页内偏移(VPO)。前面36位是虚拟页号(VPN),通过VPN可以找到相应的物理地址所在的页,如果有多级页表,VPN将会被分成多份。第i个VPN作为第i级页表的索引指向第i+1级页表的基址。最后一级页表中的PTE包含每个物理页面的页号(PPN)。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。
7.4 TLB与四级页表支持下的VA到PA的变换
在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 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。
7.5 三级Cache支持下的物理内存访问
以Core i7内存系统为例,处理器封装包括4个核,一个大的所有核共享的L3高速缓存;一个DD3的内存控制器。灭个和包括一个层次结构的TLB、一个层次结构的数据和指令高速缓存,以及一组快速的点到点链路。
得到物理地址之后,先将物理地址拆分成CT(标记)+CI(索引)+CO(偏移量),然后在一级cache内部找,如果未能寻找到标记位为有效的字节(miss)的话就去二级和三级cache中寻找对应的字节,找到之后返回结果。
7.6 hello进程fork时的内存映射
Linux通过将虚拟内存区域与磁盘上的对象关联起来以初始化这个虚拟内存区域的内容. 这个过程称为内存映射。
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。
为新进程创建虚拟内存:
- 创建当前进程的mm_struct、vm_area_struct和页表的原样副本。
- 两个进程中的每个页面都标记为只读
- 两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存, 随后的写操作通过写时复制机制创建新页面.
7.7 hello进程execve时的内存映射
加载并运行hello需要以下几个步骤:
- 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
- 创建新的区域结构:1)代码和初始化数据映射到.text和.data区(目标文件提供)2)bss和栈映射到匿名文件映射。3)共享区域:如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置PC,指向代码区域的入口点
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。故障处理具体流程如图7.8所示。
7.9 动态存储分配管理
动态储存分配管理使用动态内存分配器(如malloc)来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合。每个块就是一个连续的虚拟内存页,要么是已分配的,要么是空闲的。
已分配的块显式地保留为供应用程序使用。空闲块保持空闲,直到它显式地被应用所分配。
一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有两种基本方法与策略:
1. 带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、(可能的)额外填充以及一个字的尾部组成。
隐式空闲链表:空闲块通过头部的大小字段隐含地连接着。分配器遍历堆中所有的块,间接地遍历整个空闲块的集合。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配和最佳适配。分配器在面对释放一个已分配块时,可以合并相邻的空闲块,其中一种简单的方式,是利用隐式空闲链表的边界标记来进行合并。
2. 显式空间链表管理
显式空闲链表是将堆的空闲块组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。进行内存管理。在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章讨论了存储器地址空间,段式管理、页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时和execve时的内存映射,缺页故障与缺页中断处理和动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列:B0,B1,B2……Bm
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O 接口:
- 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。
- Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。 3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置k。
- 读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置 k 开始,然后将 k增加到k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k开始,然后更新k。
- 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
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 的当前文件位置。
8.3 printf的实现分析
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分量)。于是我们的打印字符串就显示在了屏幕上。
8.4 getchar的实现分析
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调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
8.5 本章小结
本章主要阐述了Linux的IO设备管理办法以及IO接口实现与相应的函数实现。分析了getchar()和printf()函数的实现。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
- 程序员通过IDE或者其他的文本编辑器创建了hello.c
- 文件hello.c被预处理器经过预处理调用外部库展开为hello.i
- 文件hello.i被编译器编译成hello.s,变为一个汇编语言程序
- 汇编器将hello.s汇编成机器码,把这些机器指令打包为hello.o,这个文件叫做可重定位的目标文件。
- 链接器将hello.o与动态链接库等链接成为可执行文件hello。
- 用户在shell中输入./hello 1180300918 邹建
- 在进程中shell调用fork函数创建子进程。
- 在这个子进程中,shell用函数execve来调用启动器加载器。
- 程序hello在一个时间片里执行自己的控制流,同时访问内存以及申请动态内存,并且可以接受ctrl+z,ctrl+c的信号
- hello调用sleep,getchar,exit等系统函数后进程结束,被父进程shell回收,内核删除为子进程创建的数据结构。
对计算机系统的设计与实现的感悟
计算机系统的设计思想和实现都是基于抽象实现的。从最底层的信息的表示用二进制表示抽象开始,到实现操作系统管理硬件的抽象:进程是对处理器、主存和I/O设备的抽象。虚拟内存是对主存和磁盘设备的抽象。文件是对I/O设备的抽象。计算机系统的设计精巧:为了解决快的设备存储小、存储大的设备慢的不平衡,设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度。计算机系统的设计考虑也十分全面:计算机系统设计考虑一切可能的实际情况,设计出一系列的满足不同情况的策略。比如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。
通过对hello程序的分析,成功地回忆和加深了对计算机系统所学的知识的理解。更重要的是,能够通过hello的“一生”将各个章节的知识联系起来,让我们对计算机系统有了一个全面的认识。同时,我们也学会了针对计算机系统编写出更对计算机友好的,性能更好的程序。
附件
- hello.c :hello源代码
- hello.i :预处理后的文本文件
- hello.s :编译后的汇编文件
- hello.o :汇编后的可重定位目标文件
- 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)