摘 要
摘要:本文通过hello程序从被编译、汇编、链接、运行,从外部存储设备,经过I/O桥,进入到内存,各级cache,最后在I/O中输出,最后被回收的过程中,诠释了hello简单却复杂的一生,描述了即使是最简单的程序,也在生命周期中有着同样复杂的经历,从而揭开程序由高级层次到接近底层运行机制的奥秘,加深对计算机系统的理解。
关键词:hello;linux;计算机系统;
目 录
第1章 概述
1.1 Hello简介
P2P:
(1)Program: 在 geditor 中输入代码获得hello.c程序。
(2)Process: 在 linux 中, hello.c 经过过 cpp 的预处理、 ccl 的编译、as 的汇编、 ld 的链接,最终成为了可执行目标程序 hello 。 在 shell 中输入执行命令后, shell 为其执行 fork 函数,产生子进程。
020:
(1)shell 为 hello 进程执行 execve 函数,映射虚拟内存,进入程序入口后程序开始载入物理内存。
(2)进入 main 函数执行目标代码, CPU 为运行的 hello 程序分配时间片,执行逻辑控制流。
(3)当程序运行结束后, shell 父进程负责回收 hello 进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:处理器:Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 2.59 GHz
RAM:8.00 GB (7.79 GB 可用)
系统类型:64 位操作系统, 基于 x64 的处理器
软件环境:Windows10 64位;Ubuntu 19.04
开发与调试工具:gcc,as,ld,vim, gdb, edb,readelf,VScode
1.3 中间结果
文件的作用 |
文件名 |
预处理后的文件 |
hello.i |
编译之后的汇编文件 |
hello.s |
汇编之后的可重定位目标文件 |
hello.o |
链接之后的可重定位目标文件 |
hello |
hello.o的ELF格式 |
elf.txt |
hello.o的反汇编代码 |
Disassemble_hello.s |
hello的ELF格式 |
hello_elf.elf |
hello的反汇编代码 |
hello_objdump.s |
1.4 本章小结
本章我们分析了Hello展开从P2P以及020的大概过程,介绍了作业中的硬件环境、软件环境和开发工具,最后简述了从.c文件到可执行文件中间经历的过程。
第2章 预处理
2.1 预处理的概念与作用
概念:
预处理中会展开以#开头的行,试图将其解释为预处理指令 ,其中 ISO C/C++要求支持的包括#if、 #ifdef、 #ifndef、 #else、 #elif、 #endif(条件编译)、 #define(宏定义)、 #include(源文件包含)、 #line(行控制)、 #error(错误指令)、 #pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
作用:
①将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c 第 6-8 行中的#include 等命令告诉预处理器读取系统头文件 stdio.h 、unistd.h、stdlib.h 的内容,并把它直接插入到程序文本中。
②用实际值替换用#define 定义的字符串
③根据#if 后面的条件决定需要编译的代码
④特殊符号,预编译程序可以识别一些特殊的符号, 预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i 或 cpp hello.c > hello.i
2.3 Hello的预处理结果解析
可以发现整个程序已经拓展为3060行,原来hello.c的程序出现在3047行及之后。在这之前出现的是头文件 stdio.h unistd.h stdlib.h 的依次展开。 以 stdio.h 的展开为例: stdio.h 是标准库文件, cpp 到 Ubuntu 中默认的环境变量下寻找 stdio.h,打开文件/usr/include/stdio.h ,发现其中依然使用了#define 语句,cpp 对 stdio 中的define 宏定义递归展开。 所以最终的.i 文件中是没有#define 的;发现其中使用了大量的#ifdef、#ifndef 条件编译的语句, cpp 会对条件值进行判断来决定是否执行包含其中的逻辑。预编译程序可识别一些特殊的符号,预编译程序对在源程序中出现的这些串将用合适的值进行替换。
(1)插入库所在位置:
stdio.h库文件部分
unistd.h库文件部分
stdlib.h库文件部分
(2)位置源代码:
源代码部分
2.4 本章小结
本章介绍了hello.c的预处理阶段,根据预处理命令得到了修改后的hello.i文本,并且对hello.i程序进行了预处理结果分析与,说明了了预处理器读取系统头文件中内容,并把它插入程序文本中的过程。
第3章 编译
3.1 编译的概念与作用
概念:
编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。 编译器将文本文件 hello.i 翻译成文本文件 hello.s。
作用:
编译的主要作用就是将高级语言翻译成汇编语言,不同高级语言经过编译器编译后,都输出为同一汇编语言。在此过程中,编译器将会对程序语法检查以及优化,优化过程较为复杂,比如公共子式提取,循环优化,删除无用代码,最后得到目标代码。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1常量
在if(argc!=4)中,常量4的值被保存在.text中,作为指令的一部分:
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
同理可得
for(i=0;i<8;i++){
printf("Hello %s %s\n",argv[1],argv[2]);
sleep(atoi(argv[3]));
} 里面的数字0,8,1,2,3也被保存在.text中。
在printf("用法: Hello 学号 姓名 秒数!\n");中,printf()、scanf()中的字符串常量被存储在.rodata中:
.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"
3.3.2全局变量
初始化的全局变量储存在.data节,它的初始化不需要汇编语句,而是直接完成的。
3.3.3局部变量
局部变量存储在寄存器或栈中。
程序中的int i;在下面的代码中:
.L2:
movl $0, -4(%rbp)
jmp .L3
3.3.4算术操作
在for(i=0;i<8;i++)中,循环变量i++进行了算术操作:
addl $1, -4(%rbp)
3.3.5关系操作和控制转移
在if(argc!=4)和for(i=0;i<8;i++)中使用了关系操作和控制转移,分别对应.s文件中的:
cmpl $4, -20(%rbp)
je .L2
和
cmpl $7, -4(%rbp)
jle .L4
3.3.6数组/指针/结构操作
在int main(int argc,char *argv[])中有指针数组*argv[],argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串:
.LFB6:
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)//argc存储在%edi
movq %rsi, -32(%rbp)//argv存储在%rsi
3.3.7函数操作
X86-64中过程调用传递参数的规则:第1~6个参数储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
main函数:
参数传递:
传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数调用:
被系统启动函数调用。
函数返回:
设置%eax为0并且返回,对应于return 0 。
源代码:
int main(int argc,char *argv[])
汇编代码:
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)
printf函数:
参数传递:
call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址。
函数调用:
if判断满足条件后调用,在for循环中被调用。
源代码:
(1)printf("用法: Hello 学号 姓名 秒数!\n");
(2)printf("Hello %s %s\n",argv[1],argv[2]);
汇编代码:
(1).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"
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
(2).L4:
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
call printf@PLT
exit函数:
参数传递:
传入的参数为1,再执行退出命令。
函数调用:
if判断条件满足后被调用。
源代码:
exit(1);
汇编代码:
.LFB6:
movl $1, %edi
call exit@PLT
sleep函数:
参数传递:
传入参数atoi(argv[3])。
函数调用:
for循环下被调用,call sleep。
源代码:
sleep(atoi(argv[3]));
汇编代码:
.L4:
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
getchar函数:
函数调用:
在main中被调用,call getchar。
源代码:
getchar();
汇编代码:
.L3:
call getchar@PLT
3.4 本章小结
本章主要介绍了编译的概念以及过程。通过hello程序表现了c语言如何转换成为汇编代码。介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环。编译程序所做的工作,就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码表示。
第4章 汇编
4.1 汇编的概念与作用
概念:
驱动程序运行汇编器as,将汇编语言(这里是hello.s)编写的程序翻译成机器语言(hello.o)编写的程序的过程称为汇编,同时这个机器语言文件也是可重定位目标文件。
作用:
汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
readelf -a hello.o > ./elf.txt
4.3.1 ELF头:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1240 (bytes into file)
Flags: 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头大小,节的大小和数量等等一系列信息。
4.3.2 节头部表:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 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
000000000000002b 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000136
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文件中出现的各个节的类型、位置、所占空间大小等信息。
4.3.3 重定位节:
Relocation section '.rela.text' at offset 0x388 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
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
Relocation section '.rela.eh_frame' at offset 0x448 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
表述了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。
4.3.4 符号表
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
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > Disassemble_hello.s
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 :
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
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20
1c: R_X86_64_PC32 .rodata-0x4
20: e8 00 00 00 00 callq 25
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi
2a: e8 00 00 00 00 callq 2f
2b: R_X86_64_PLT32 exit-0x4
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
36: eb 48 jmp 80
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
54: R_X86_64_PC32 .rodata+0x22
58: b8 00 00 00 00 mov $0x0,%eax
5d: e8 00 00 00 00 callq 62
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
71: R_X86_64_PLT32 atoi-0x4
75: 89 c7 mov %eax,%edi
77: e8 00 00 00 00 callq 7c
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
86: e8 00 00 00 00 callq 8b
87: R_X86_64_PLT32 getchar-0x4
8b: b8 00 00 00 00 mov $0x0,%eax
90: c9 leaveq
91: c3 retq
分析hello.o的反汇编,与第3章的 hello.s进行对照分析:
(1)数的表示:
hello.s中的操作数是十进制,hello.o反汇编代码中的操作数是十六进制。
(2)分支转移:
跳转语句之后,hello.s中是.L2和.LC1等段名称,而反汇编代码中跳转指令之后是PC相对偏移的地址,即间接地址。
(3)函数调用:
hello.s中call指令使用的是函数名称,而反汇编代码中call指令使用的是main函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.5 本章小结
在本章通过使用汇编器(as)将hello.s汇编成二进制的机器指令,形成的是一个可重定位目标文件,这个文件可以使用工具readelf来查看与分析,我们查看了可重定位目标elf格式的文件头、节头段、重定位段、符号表,获得了相关节的位置、大小,符号条目以及他们的大小和重定位信息……其次我们将hello.o文件利用objdump进行反汇编同样的得到了一个反汇编文件,将其与编译得到的文件进行比较,并分析了他们在函数调用以及一些符号引用上的差异。
第5章 链接
5.1 链接的概念与作用
链接器(1d)负责处理将printf.o等c语言库函数的已经单独编译好的目标文件与hello.o进行合并。结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。
作用:
由于链接是由链接器程序执行的,链接器使得分离编译成为可能,这样不仅节省了大量的共享代码所占用的空间,而且使得编译的效率有了很大的提升,向库函数这样就不用再次对它进行编译了。而这对于开发和维护大型的程序具有很重要的意义。
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
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello_elf.elf
5.3.1 ELF头:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4010f0
Start of program headers: 64 (bytes into file)
Start of section headers: 14208 (bytes into file)
Flags: 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
对比之前用hello.o形成的elf文件发现文件头发生了一些变化:
①类型发生改变汇编之后是可重定位文件,而现在变成了可执行文件。
②程序头大小改变,原本没有程序头现在程序头56个字节。
③程序的入口地址发生改变,原本程序头与程序入口的地址均为0,现在都已经有了确切的地址,指明了程序第一条语句的地址。
④节头数量与字符串表索引节头改变,elf节的数量改变
5.3.2 节头部表
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 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
000000000000002a 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
原本只有14个节而现在增至27个节,而且已经没有重定位即rel节因为行成可执行文件之后已经完成重定位。多出许多节与我们连接的库函数有关。而且其中包括size大小和在程序中的偏移量offset,因为是已链接的程序,所以根据标出的信息就可以确定程序实际被加载到虚拟地址的地址。
5.3.3 符号表:
Symbol table '.symtab' contains 51 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004002e0 0 SECTION LOCAL DEFAULT 1
2: 0000000000400300 0 SECTION LOCAL DEFAULT 2
3: 0000000000400320 0 SECTION LOCAL DEFAULT 3
4: 0000000000400340 0 SECTION LOCAL DEFAULT 4
5: 0000000000400378 0 SECTION LOCAL DEFAULT 5
6: 0000000000400398 0 SECTION LOCAL DEFAULT 6
7: 0000000000400470 0 SECTION LOCAL DEFAULT 7
8: 00000000004004cc 0 SECTION LOCAL DEFAULT 8
9: 00000000004004e0 0 SECTION LOCAL DEFAULT 9
10: 0000000000400500 0 SECTION LOCAL DEFAULT 10
11: 0000000000400530 0 SECTION LOCAL DEFAULT 11
12: 0000000000401000 0 SECTION LOCAL DEFAULT 12
13: 0000000000401020 0 SECTION LOCAL DEFAULT 13
14: 0000000000401090 0 SECTION LOCAL DEFAULT 14
15: 00000000004010f0 0 SECTION LOCAL DEFAULT 15
16: 0000000000401238 0 SECTION LOCAL DEFAULT 16
17: 0000000000402000 0 SECTION LOCAL DEFAULT 17
18: 0000000000402040 0 SECTION LOCAL DEFAULT 18
19: 0000000000403e50 0 SECTION LOCAL DEFAULT 19
20: 0000000000403ff0 0 SECTION LOCAL DEFAULT 20
21: 0000000000404000 0 SECTION LOCAL DEFAULT 21
22: 0000000000404048 0 SECTION LOCAL DEFAULT 22
23: 0000000000000000 0 SECTION LOCAL DEFAULT 23
24: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
25: 0000000000000000 0 FILE LOCAL DEFAULT ABS
26: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
27: 0000000000403e50 0 OBJECT LOCAL DEFAULT 19 _DYNAMIC
28: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
29: 0000000000404000 0 OBJECT LOCAL DEFAULT 21 _GLOBAL_OFFSET_TABLE_
30: 0000000000401230 5 FUNC GLOBAL DEFAULT 15 __libc_csu_fini
31: 0000000000404048 0 NOTYPE WEAK DEFAULT 22 data_start
32: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
33: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 _edata
34: 0000000000401238 0 FUNC GLOBAL HIDDEN 16 _fini
35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
36: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
37: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 22 __data_start
38: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@@GLIBC_2.2.5
39: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
40: 0000000000402000 4 OBJECT GLOBAL DEFAULT 17 _IO_stdin_used
41: 00000000004011c0 101 FUNC GLOBAL DEFAULT 15 __libc_csu_init
42: 0000000000404050 0 NOTYPE GLOBAL DEFAULT 22 _end
43: 0000000000401120 5 FUNC GLOBAL HIDDEN 15 _dl_relocate_static_pie
44: 00000000004010f0 47 FUNC GLOBAL DEFAULT 15 _start
45: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 __bss_start
46: 0000000000401125 146 FUNC GLOBAL DEFAULT 15 main
47: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@@GLIBC_2.2.5
48: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5
49: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@@GLIBC_2.2.5
50: 0000000000401000 0 FUNC GLOBAL HIDDEN 12 _init
符号表中的条目也由原来的18条变为现在的51条,在链接过程中加入了许多新的符号。
5.4 hello的虚拟地址空间
通过分析典型的ELF可执行文件目标文件个结构可以知道这些节最后加载到虚拟内存空间为两个段——代码段和数据段,而且这两个段的起始地址可以通过5.3的分析得出。
查看ELF文件中的程序头,这是在程序被执行的时候告诉链接器运行时需要加载的内容并提供动态链接信息。每个表项提供了各段在虚拟地址空间中的大小,位置,访问权限和对齐方式。
下面使用Edb打开hello程序,通过Data Dump窗口可以查看加载到虚拟地址中的hello程序。
可以对这个datadump的开始的字节与elf头的magic进行对比可以发现,二者相同,说明程序正是从0x400000开始加载,而这段数据也正描述了整个elf文件的一些总体信息。并且代码段也开始于0x400000。其次我们观察0x403e50开始的地址空间代表了数据段。
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello_objdump.s
分析hello与hello.o的不同:
①链接增加新的函数:
在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
②增加的节:
hello中增加了.init和.plt节,和一些节中定义的函数。
③函数调用:
hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
④地址访问:
hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
链接的过程:
根据hello和hello.o的不同,分析出链接的过程为:
链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。
5.6 hello的执行流程
使用edb单步调试运行程序,观察其调用的函数,这里可以发现在调用main之前主要进行了初始化的工作,调用了_init,在这个函数之后动态链接的重定位工作已经完成,我们可以看到在这个函数的调用之后是一系列在这个程序中所用到的库函数(printf,exit,atoi等等),这些函数实际上在代码段并不占用实际的空间只是一个占位符,实际上他们的内容在共享区(高地址)处。之后调用了_start即为起始的地址,准备开始执行main的内容,在执行main之后还会执行__libc_csu_init 、__libc_csu_fini 、_fini等函数,最终这个程序才结束。
下面列出了各个函数的名称与地址:
- _init <0x0000000000401000>
- puts@plt <0x0000000000401030>
- printf@plt <0x0000000000401040>
- getchar@plt <0x0000000000401050>
- atoi@plt <0x0000000000401060>
- exit@plt <0x0000000000401070>
- sleep@plt <0x0000000000401080>
- _start <0x00000000004010f0>
- _dl_relocate_static_pie <0x0000000000401120>
- main <0x0000000000401125>
- __libc_csu_init <0x00000000004011c0>
- __libc_csu_fini <0x0000000000401230>
- _fini <0x0000000000401238>
5.7 Hello的动态链接分析
[20] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[21].got.plt PROGBITS 0000000000404000 00003000
进入edb查看:
1.执行_init之前:
2.执行_init之后:
通过对两个图的对比,注意到0x404000开始的地方内发生了变化,由前面的分析可以知道GOT表的位置就在0x404000处,这里执行ld_init之后填入了GOT的信息。
对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。
5.8 本章小结
本章主要了解了在linux中链接的过程。通过查看hello的虚拟地址空间,并且对比hello与hello.o的反汇编代码,更好地掌握了链接与之中重定位的过程、执行流程以及动态链接分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典的定义是一个执行中的程序的实例。
作用:
(1)每次运行程序时,shell创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
(2)进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1.shell定义
Linux系统中,Shell是一个交互型应用级程序,提供了用户与内核进行交互操作的一种接口。代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程。
Shell有许多形式:
(1)sh 最早的shell
(2)csh/tcsh 变种
(3)bash变种、缺省的Linux shell
6.2.2.shell的功能
Shell是一种命令解释器,它解释由用户输入的命令并且把它们送到内核去执行。Shell可以解析命令行将其划分为各个字符串存在main的参数argv[]中,对于各个命令,shell负责判断命令的正确性以及分类(内置还是可执行文件等),并execve加载运行这个可执行程序,同时shell还可以处理各种信号,负责回收终止的子程序,防止僵死进程的出现。(以下来自百度百科)除此之外,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果。
6.2.3.shell的处理流程
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,讲命令行分解以空格为间隙分解,获取命令行参数,并构造传递给execve的argv向量。
(3)检查第一个命令行参数是否是一个内置的shell命令,如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。
(4)如果是一个路径下的可执行文件,调用fork( )创建新进程/子进程。
(5)在子进程中,用步骤b获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回。
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
Hello的fork过程:
根据shell的处理流程,输入命令执行hello后,父进程如果判断不是内部指令,会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同或许在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4 Hello的execve过程
execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。该函数的作用就是在当前进程的上下文中加载并运行一个新的程序。
只有当出现错误时,例如找不到Hello时,execve才会返回到调用程序,这里与一次调用两次返回的fork不同。
在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:
int main(intargc , char **argv , char *envp);
结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello:
①删除已存在的用户区域(自父进程独立)。
②映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
③映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
④设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
一系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。
用户模式和内核模式:
处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文:
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
调度的过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。
示例:sleep进程的调度过程
①初始时,控制流再hello内,处于用户模式.
②调用系统函数sleep后,进入内核态,此时间片停止。
③设定的秒数后,发送中断信号,转回用户模式,继续执行指令。
用户态与核心态转换:
为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创世模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
异常类型:
类别 |
原因 |
异步/同步 |
返回行为 |
中断 |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
故障 |
潜在可恢复的错误 |
同步 |
可能返回到当前指令 |
终止 |
不可恢复的错误 |
同步 |
不会返回 |
处理方式:
1.中断:
2.陷阱:
3.故障:
4.终止:
按下Ctrl+Z:进程收到 SIGSTP 信号, hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是2681;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台。
Ctrl+C:进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。
中途乱按:只是将屏幕的输入缓存到缓冲区。乱码被认为是命令。
Kill命令:挂起的进程被终止,在ps中无法查到到其PID。
6.7本章小结
本章介绍了hello程序在计算中的加载与运行的过程。计算机为每个运行中的程序抽象出一个概念为进程。Hello是以进程的形式运行,每个进程都处在某个进程的上下文中,每个进程也都有属于自己的上下文,用于操作系统通过上下文切换进行进程调度。用户通过shell和操作系统交互,向内核提出请求,shell通过fork函数和execve函数来运行可执行文件。操作系统中有一套异常控制的系统,用于保障程序运行。异常的种类分为较低级的中断,终止,陷阱和故障,还有较高级的上下文切换和信号机制。通过对hello执行过程中对其发送各种信号,对各种信号的处理方式有了更深的理解。
第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。
逻辑地址(Logical Address):
逻辑地址是指由程序hello产生的与段相关的偏移地址部分(hello.o)
线性地址(Linear Address):
线性地址是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
虚拟地址(Virtual Address):
有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
把Base + offset,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
优点:
①由于它不要求作业或进程的程序段和数据在内存中连续存放,从而有效地解决了碎片问题。
②动态页式管理提供了内存和外存统一管理的虚存实现方式,使用户可以利用的存储空间大大增加。这既提高了主存的利用率,又有利于组织多道程序执行。
缺点:
①要求有相应的硬件支持。例如地址变换机构,缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持,这增加了机器成本。
②增加了系统开销,例如缺页中断处理机。
③请求调页的算法如果选择不当,有可能产生抖动现象。
④虽然消除了碎片,但每个作业或进程的最后一页内总有一部分空间得不到利用,如果页面较大,则这一部分的损失仍然较大。
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
多级页表:
将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。
解析VA,利用前m位vpn寻找一级页表位置,接着重复k-1次,在第k级页表获得了PTE,将PPN与VPO组合获得PA。
7.5 三级Cache支持下的物理内存访问
CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位),CI(组索引),CO(偏移量)。根据CI寻找到正确的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次前往L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的页面标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
在当前进程中的程序执行了execve(”a.out”,NULL, NULL)调用时,execve函数在当前程序中加载并运行包含在可执行文件a.out中的程序,用a.out代替了当前程序。
加载并运行a.out主要分为一下几个步骤:
①删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构;
②映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零;
③映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内;
④设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。
会出现缺页异常的情况:
①线性地址不在虚拟地址空间中。
②线性地址在虚拟地址空间中,但没有访问权限。
③没有与物理地址建立映射关系fork等系统调用时并没有映射物理页,写数据->缺页异常->写时拷贝。
④映射关系建立了,但在交换分区中,页面访问权限不足。
缺页中断处理:
①处理器生成一个虚拟地址,并将它传送给MMU。
②MMU生成PTE地址,并从高速缓存/主存请求得到它。
③高速缓存/主存向MMU返回PTE。
④PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
⑤缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
⑥缺页处理程序页面调入新的页面,并更新内存中的PTE。
⑦缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。不同系统有微小的差别但是一般来说堆存在于未初始化的数据后面并想高地址生长。对于每个进程,内核负责维护一个变量叫brk,它指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护。每个块都是一个连续的虚拟内存片有两种状态(已分配或空闲)顾名思义,已分配的块已经被进程所使用,而空闲块可以用来分配,这时他就变成了一个已分配块,而已分配的块只有在被释放才能重新在使用。
根据分配器的风格来分类可以分为显式分配器与隐式分配器:
(1)显式分配器:
要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。必须用户来主动来调用某些函数来进行内存的申请与释放。
(2)隐式分配器:
这时需要分配器检测一个已分配块何时不再被程序所使用,那么久释放这个块,也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
碎片:
造成堆利用效率低的原因是碎片现象,碎片分为两种:
(1)内部碎片:当一个已分配块比有效载荷的,由对齐要求产生。
(2)外部碎片:空闲内存合起来足够满足分配请求,但是处于不连续的内存片中。
分配器的设计可以有多种方式针对空闲块的组织方法有以下三种:
(1)隐式空闲链表(implicit free list)
隐含链表方式即在每一块空闲或被分配的内存块中使用一个字的空间来保存此块大小信息和标记是否被占用。根据内存块大小信息可以索引到下一个内存块,这样就形成了一个隐含链表,通过查表进行内存分配。优点是简单,缺点就是慢,需要遍历所有。
(2)显式空闲链表(explicit free list)
显示空闲链表的方法,和隐含链表方式相似,唯一不同就是在空闲内存块中增加两个指针,指向前后的空闲内存块。相比显示链表,就是分配时只需要顺序遍历空闲块,虽然在空间上的开销有所增大,但是放置以及合并操作所用到的时间会有所减少。
(3)分离空闲链表(segregated free list)
分配器维护多个空闲链表,其中每个链表中的块大小大致相等,即讲这些空闲块分成一些等价类,先按照大小进行索引找到相应的空闲链表,在链表内部搜索合适的块,这样相比于显式空闲链表时间效率更高。
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、 intel 的段式管理、 hello 的页式管理,在指定环境下介绍了 VA 到 PA 的变换、物理内存访问,还介绍 hello 进程 fork 时的内存映射、 execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
文件(所有的I/O设备都被模型化为文件,甚至内核也被映射为文件)
设备管理
unix io接口
这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
(1)打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
(3)改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
(4)读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k。
(5)关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中。
Unix IO函数:
① open()函数
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式。
返回值:成功:返回文件描述符;失败:返回-1。
② close()函数
功能描述:
用于关闭一个被打开的的文件。
所需头文件:
#include
函数原型:
int close(int fd)
参数:
fd文件描述符。
函数返回值:
0成功,-1出错。
③ read()函数
功能描述:
从文件读取数据。
所需头文件:
#include
函数原型:
ssize_t read(int fd, void *buf, size_t count);
参数:
fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
返回值:
返回所读取的字节数;0(读到EOF);-1(出错)。
④ write()函数
功能描述:
向文件写入数据。
所需头文件:
#include
函数原型:
ssize_t write(int fd, void *buf, size_t count);
返回值:
写入文件的字节数(成功);-1(出错)。
⑤ lseek()函数
功能描述:
用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:
#include ,#include
函数原型:
off_t lseek(int fd, off_t offset,int whence);
参数:
fd:文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)。
返回值:
成功:返回当前位移;失败:返回-1。
8.3 printf的实现分析
printf函数:
int printf(const char *fmt, ...)
{
int i;
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
所引用的vsprintf函数
int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
chartmp[256];
va_listp_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != '%')
{
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt)
{
case 'x':
itoa(tmp, *((int *)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
return (p - buf);
}
}
printf函数将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
首先我们看一下getchar()函数的具体实现代码,主要通过调用read函数以及一些控制的代码来实现。
#include "sys/syscall.h"
#include
int getchar(void)
{
char c;
return (read(0,&c,1)==1)?(unsigned char)c:EOF
//EOF定义在stdio.h文件中
}
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装,大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程。
结论
hello的一生:
①hello.c经过预编译,得到hello.i文本文件
②hello.i经过编译,得到hello.s汇编文件
③hello.s经过汇编,得到二进制可重定位目标文件hello.o
④hello.o经过链接,生成了可执行文件hello
⑤bash进程调用fork函数,生成子进程;并由execve函数加载运行当前进程的上下文中加载并运行hello程序。
⑥hello的变化过程中,会出现各种地址,但我们真正关心的是物理地址。
⑦hello在运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟密切相关。
⑧hello最终被shell父进程回收,内核会收回为其创建的所有信息。
hello的一生,充分诠释了计算机系统的内涵,用自己的生命展现真理。我们计算机的学生,无时无刻不敲打着代码,虽然渺小,但是否也能发挥自己的价值呢?我们的一生,和hello一样,也许正在诠释着这个问题。
附件
文件的作用 |
文件名 |
预处理后的文件 |
hello.i |
编译之后的汇编文件 |
hello.s |
汇编之后的可重定位目标文件 |
hello.o |
链接之后的可执行目标文件 |
hello |
Hello.o 的 ELF 格式 |
elf.txt |
Hello.o 的反汇编代码 |
Disassemble_hello.s |
hello的ELF 格式 |
hello_elf.elf |
hello 的反汇编代码 |
hello_objdump.s |
参考文献
[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] 兰德尔E.布莱恩特 大卫R.奥哈拉伦 深入理解计算机系统(第三版)
[8] https://www.cnblogs.com/pianist/p/3315801.html printf函数的深入剖析
[9] 《步步惊芯——软核处理器内部设计分析》 TLB的作用及工作过程
[10] https://blog.csdn.net/qq_35066345/article/details/78747410 动态存储器分配:内存动态分区分配方式的理解以及模拟
[11] https://blog.csdn.net/hunter___/article/details/82963577 可执行程序加载到内存的过程