第1章 概述
1.1 Hello简介
Hello的P2P(From Program to Process)过程
- 编写步调(Program):菜鸟步调员(或初学者)使用文本编辑器(如Vim、VS Code等)编写Hello步调,并将其生存为文件(如hello.c)。
- 预处理惩罚(Preprocessing):编译器(如gcc)首先处理惩罚hello.c文件,处理惩罚宏界说、文件包罗等预处理惩罚指令,生成预处理惩罚后的文件(通常是.i文件,但通常这一步是隐式的)。
- 编译(Compilation):编译器将预处理惩罚后的文件翻译成汇编代码(.s文件,同样这一步通常是隐式的)。汇编代码是低级呆板指令的文本表示,但仍然是人类可读的。
- 汇编(Assembly):汇编器将汇编代码转换成呆板代码(二进制),生成目标文件(.o或.obj文件)。
- 链接(Linking):链接器将目标文件与其他必要的库文件(如C标准库)链接在一起,生成可实行文件(如hello)。
- 步调加载(Program Loading):当用户在Shell(如Bash)中输入./hello时,Shell通过系统调用(如execve)请求操作系统加载并实行hello步调。操作系统为hello步调创建一个新的历程(Process),并为其分配必要的资源(如内存、文件形貌符等)。
- 历程实行(Process Execution):操作系统通过历程调度(如时间片轮转)使hello历程在CPU上运行。Hello历程实行其呆板代码,输出“Hello, World!”到标准输出(通常是屏幕)。
- 历程终止(Process Termination):Hello历程实行完毕后,操作系统回收其资源,并竣事历程。
Hello的O2O(From Zero-0 to Zero-0)过程
- 从0开始(From Zero-0):Hello步调在磁盘上以文件形式存在,占用一定的存储空间(但不是零)。这里的“0”指的是步调实行前的初始状态,即未加载到内存、未实行的状态。
- 加载到内存(Loading to Memory):当hello步调被加载到内存时,它占据了一段连续的内存空间。操作系统通过页表、TLB等机制管理捏造地点到物理地点的映射,确保hello步调可以或许正确地访问内存。
- 实行与输出(Execution and Output):Hello历程在CPU上实行,通过IO系统调用与硬件交互,将“Hello, World!”输出到屏幕。
- 资源回收与竣事(Resource Recycling and Termination):Hello历程实行完毕后,操作系统回收其占用的内存、文件形貌符等资源。终极,hello步调的状态回到“0”,即不再占用任何系统资源,完成了从0到0的循环。
1.2 环境与工具
硬件环境:处理惩罚器:11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz 2.80 GHz
RAM:16.0 GB系统范例:64位操作系统,基于x64的处理惩罚器
软件环境:Windows11 64位;Ubuntu 22.04.4
开发与调试工具:gcc,as,ld,vim,edb,readelf,VScode
1.3 中间结果
1.4 本章小结
本章对hello进行了扼要的先容,首先先容了hello的P2P,020过程,后先容了作业中的硬件环境、软件环境和开发工具,最后列出了从hello.c文件到hello可实行文件的中间文件以及文件的作用
第2章 预处理惩罚
2.1 预处理惩罚的概念与作用
概念:
预处理惩罚(Preprocessing)指的是在步调源代码被翻译为目标代码的过程中,生成二进制代码之前的一个准备阶段。在这个阶段,预处理惩罚器(Preprocessor)对步调源代码文本进行处理惩罚,得到的结果再由编译器焦点进一步编译。预处理惩罚并不对步调的源代码进行剖析,而是将其分割或处理惩罚成为特定的单位(在C/C++中称为预处理惩罚暗号),以支持语言特性,如宏调用等。
作用:
1、通过#define指令界说宏,并在代码中使用宏来替换特定的值或代码片段。这有助于简化代码,提高代码的可读性和可维护性。
2、使用#include指令将其他文件的内容包罗到当前文件中。这有助于实当代码的模块化和复用,使代码结构更加清晰。
3、使用#if、#ifdef、#else等指令实现根据条件选择性地编译不同的代码块。这有助于在不同的实行环境中方便地修改或编译源代码。
2.2在Ubuntu下预处理惩罚的命令
命令:cpp hello.c > hello.i
图 1 预处理惩罚命令
2.3 Hello的预处理惩罚结果剖析
在hello.i文件中首先出现的是头文件 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 会对条件值进行判断来决定是否实行包罗其中的逻辑。预编译步调可识别一些特殊的符号,预编译步调对在源步调中出现的这些串将用合适的值进行替换。
插入库所在位置:
图 2 hello.i库文件部分
库中预置函数:
hello.i声明函数部分
源代码位置:
图 4 hello.i源代码部分
2.4 本章小结
本章主要先容的是预处理惩罚的概念和作用,以及Ubuntu下预处理惩罚的指令(cpp hello.c > hello.i),同时我们检察了hello.c文件的预处理惩罚结果hello.i 文本文件,对预处理惩罚有了深刻的认识。
第3章 编译
3.1 编译的概念与作用
概念:
编译是将高级编程语言(如C、C++、Java等)编写的源代码转换为呆板代码(也称为目标代码或可实行代码)的过程。这个过程通常由编译器完成,编译器是一种软件工具,它可以或许读取源代码,分析其结构,并将其转换为目标代码。呆板代码是盘算机硬件可以直接实行的二进制指令,这些指令存储在存储器中,并由CPU逐条读取和实行。
作用:
1、提高步调的实行效率,编译过程中,编译器会对源代码进行优化,生成更高效的呆板代码。
2、增强步调的可移植性,编译器可以针对特定的硬件平台生成优化的呆板代码,以确保步调可以或许在目标硬件上高效运行。
3、提供高级抽象和错误检查,高级编程语言提供了更高的抽象层次,使步调员可以或许使用更易于理解和编写的代码来开发步调。
4、支持模块化编程和代码复用,编译过程还涉及链接阶段,将生成的呆板代码与其他库文件链接,生成终极的可实行文件。
5、提高代码的安全性和稳固性,编译过程中的各种检查(如语法检查、范例检查等)可以发现并报告源代码中的潜伏问题,从而提高代码的安全性和稳固性。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图 5 编译命令
3.3 Hello的编译结果剖析
汇编代码:
.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 \346\211\213\346\234\272\345\217\267 \347\247\222\346\225\260\357\274\201"
.LC1:
.string "Hello %s %s %s\n"
.text
.globl main
.type main, @function
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)
cmpl $5, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
.L2:
movl $0, -4(%rbp)
jmp .L3
.L4:
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rcx
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
movq -32(%rbp), %rax
addq $32, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
addl $1, -4(%rbp)
.L3:
cmpl $8, -4(%rbp)
jle .L4
call getchar@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE6:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
3.3.1段和标签
.file "hello.c": 指明这个文件是从hello.c源文件编译而来的。
.text: 标记代码段的开始,这个段包罗了步调的可实行指令。
.section .rodata: 标记只读数据段的开始,这个段包罗了步调中的常量数据。
.align 8: 确保接下来的数据或指令在8字节边界上对齐。
.LC0 和 .LC1: 是字符串常量的标签,用于在步调中引用这些字符串。
3.3.2字符串常量
.string "\347\224\250\346\263\225: ...": 这是一个UTF-8编码的字符串,由于它包罗了非ASCII字符(看起来像是中文),以是以八进制形式表示。
.string "Hello %s %s %s\n": 这是一个标准的C字符串,用于printf函数格式化输出。
3.3.3主函数 main
.globl main: 声明main函数是全局可见的,即它是步调的入口点。
.type main, @function: 指定main是一个函数。
main:: main函数的开始。
3.3.4函数体
函数体内部包罗了一系列指令,用于设置栈帧、传递参数、条件判断、函数调用等。
X86-64中,过程调用传递参数规则:第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数生存在栈当中。
endbr64: 是一个安全相干的指令,用于增强对控制流劫持攻击(如ROP攻击)的防御。
pushq %rbp 和 movq %rsp, %rbp: 设置新的栈帧。
subq $32, %rsp: 为局部变量分配栈空间。
cmpl $5, -20(%rbp): 比力传入的参数数量(可能是argc)和5。
je .L2: 如果相等,跳转到.L2标签处。
leaq .LC0(%rip), %rdi: 将字符串.LC0的地点加载到%rdi寄存器中,准备调用puts函数。
call puts@PLT: 调用puts函数打印字符串.LC0。
movl $1, %edi 和 call exit@PLT: 调用exit函数退出步调,返回状态码1。
3.3.5循环和条件判断
.L2:: 跳转目标,开始处理惩罚argc不等于5的情况。
接下来的代码块包罗了一个循环,该循环使用printf、atoi和sleep函数处理惩罚传入的参数,并打印格式化的字符串。
movl $0, -4(%rbp): 初始化循环计数器。
.L3: 和 .L4:: 循环的开始和竣事标签。
循环内部,通过加载参数、格式化字符串和调用printf来输出信息。
使用atoi将字符串转换为整数,然后调用sleep函数。
循环直到计数器到达8为止。
3.3.6函数竣事
all getchar@PLT: 在循环竣事后,调用getchar函数,可能是为了等待用户输入或同步输出。
movl $0, %eax: 设置返回值0。
leave: 规复栈帧。
ret: 返回调用者。
3.4 本章小结
本章深入阐述了编译的根本概念及其流程,并通过一个示例函数生动展示了C语言如何逐步转化为汇编代码的过程。内容不仅涵盖了汇编代码中变量、常量的实现方式,以及参数的传递机制,还详细解说了分支和循环在汇编层面的实现。编译步调的焦点使命在于,通过严谨的词法分析和语法分析,确保每一条指令都严酷遵循语法规则,随后将其精准地翻译成等价的中间代码或汇编代码。这一过程,包括之前对编译结果的细致剖析,都极大地加深了我对C语言数据结构与操作的理解,使我对C语言到汇编语言的转换有了更为踏实的把握。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编语言是一种低级编程语言,与盘算机硬件结构紧密相干。它使用助记符来表示呆板指令,这些助记符是易于人类理解和记忆的符号,如“MOV”代表数据移动,“ADD”代表加法。汇编语言指令几乎可以一一对应到呆板码指令。
作用:
汇编是一个关键过程,其焦点在于将高级语言的表述转化为盘算机可以或许直接识别并实行的代码文件。这一转化使命主要由汇编器承担,它负责将后缀为“.s”的汇编步调代码精确地翻译成呆板语言指令。随后,这些指令会被经心打包成可重定位的目标步调格式,并妥善生存在后缀为“.o”的目标文件中。值得注意的是,“.o”文件实质上是一个二进制文件,其内部蕴含着步调的指令编码,这些编码是盘算机可以或许直接解读并实行的焦点所在。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
图 6 汇编命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的根本信息,特别是重定位项目分析。
命令:readelf -a hello.o > ./elf.txt
图 7 生成并导出elf文件命令
elf文本文件包罗了系统信息,编码方式,ELF头大小,节的大小和数量等等一系列信息。
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
种别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
范例: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地点: 0x0
步调头起点: 0 (bytes into file)
Start of section headers: 1264 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
节头:
[号] 名称 范例 地点 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000009d 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000003a0
00000000000000c0 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000dd
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000dd
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000e0
0000000000000040 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 00000120
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000014c
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 00000150
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000170
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000460
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 000001a8
00000000000001b0 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000358
0000000000000048 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000478
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
There are no section groups in this file.
本文件中没有步调头。
There is no dynamic section in this file.
重定位节 '.rela.text' at offset 0x3a0 contains 8 entries:
偏移量 信息 范例 符号值 符号名称 + 加数
00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000021 000c00000004 R_X86_64_PLT32 0000000000000000 puts - 4
00000000002b 000d00000004 R_X86_64_PLT32 0000000000000000 exit - 4
00000000005f 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 2c
000000000069 000e00000004 R_X86_64_PLT32 0000000000000000 printf - 4
00000000007c 000f00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
000000000083 001000000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000092 001100000004 R_X86_64_PLT32 0000000000000000 getchar - 4
重定位节 '.rela.eh_frame' at offset 0x460 contains 1 entry:
偏移量 信息 范例 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
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 157 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
No version information found in this file.
Displaying notes found in: .note.gnu.property
全部者 Data size Description
GNU 0x00000010 NT_GNU_PROPERTY_TYPE_0
Properties: x86 feature: IBT, SHSTK
4.4 Hello.o的结果剖析
命令:objdump -d -r hello.o > disas_hello.s
反汇编代码:
hello.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 20 sub $0x20,%rsp
c: 89 7d ec mov %edi,-0x14(%rbp)
f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
13: 83 7d ec 05 cmpl $0x5,-0x14(%rbp)
17: 74 16 je 2f <main+0x2f>
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
1c: R_X86_64_PC32 .rodata-0x4
20: e8 00 00 00 00 callq 25 <main+0x25>
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi
2a: e8 00 00 00 00 callq 2f <main+0x2f>
2b: R_X86_64_PLT32 exit-0x4
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
36: eb 53 jmp 8b <main+0x8b>
38: 48 8b 45 e0 mov -0x20(%rbp),%rax
3c: 48 83 c0 18 add $0x18,%rax
40: 48 8b 08 mov (%rax),%rcx
43: 48 8b 45 e0 mov -0x20(%rbp),%rax
47: 48 83 c0 10 add $0x10,%rax
4b: 48 8b 10 mov (%rax),%rdx
4e: 48 8b 45 e0 mov -0x20(%rbp),%rax
52: 48 83 c0 08 add $0x8,%rax
56: 48 8b 00 mov (%rax),%rax
59: 48 89 c6 mov %rax,%rsi
5c: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 63 <main+0x63>
5f: R_X86_64_PC32 .rodata+0x2c
63: b8 00 00 00 00 mov $0x0,%eax
68: e8 00 00 00 00 callq 6d <main+0x6d>
69: R_X86_64_PLT32 printf-0x4
6d: 48 8b 45 e0 mov -0x20(%rbp),%rax
71: 48 83 c0 20 add $0x20,%rax
75: 48 8b 00 mov (%rax),%rax
78: 48 89 c7 mov %rax,%rdi
7b: e8 00 00 00 00 callq 80 <main+0x80>
7c: R_X86_64_PLT32 atoi-0x4
80: 89 c7 mov %eax,%edi
82: e8 00 00 00 00 callq 87 <main+0x87>
83: R_X86_64_PLT32 sleep-0x4
87: 83 45 fc 01 addl $0x1,-0x4(%rbp)
8b: 83 7d fc 08 cmpl $0x8,-0x4(%rbp)
8f: 7e a7 jle 38 <main+0x38>
91: e8 00 00 00 00 callq 96 <main+0x96>
92: R_X86_64_PLT32 getchar-0x4
96: b8 00 00 00 00 mov $0x0,%eax
9b: c9 leaveq
9c: c3 retq
分析:
1、呆板语言的构成
呆板语言是由一系列二进制指令构成的,每条指令代表了一个具体的操作,好比数据传输、算术运算、控制转移等。在x86-64架构中,每条指令通常由操作码(opcode)和操作数(operands)组成。操作码指明确要实行的操作范例,而操作数则指定了操作的对象。
2、汇编语言与呆板语言的映射关系
汇编语言是一种低级编程语言,它使用助记符(mnemonic)来代表呆板指令的操作码,并且答应使用符号名来代表内存地点和寄存器。汇编器(assembler)将汇编代码转换为呆板代码。
3、操作数的差异
在汇编代码中,操作数通常是以人类可读的形式(如寄存器名、符号地点等)给出的。而在呆板代码中,这些操作数被编码为二进制值。比方,在汇编代码中,mov %rsp, %rbp 表示将寄存器 rsp 的值移动到寄存器 rbp 中;在呆板代码中,这个操作被编码为 48 89 e5。
4、分支转移、函数调用等指令
对于分支转移和函数调用等指令,汇编代码和呆板代码之间也存在类似的映射关系。比方:分支转移指令(如 je、jle 等)在汇编代码中表示条件跳转,而在呆板代码中则通过特定的操作码和跳转目标地点(通常是相对于当前指令的偏移量)来实现。函数调用指令(如 callq)在汇编代码中表示调用一个函数,而在呆板代码中则通过操作码和函数地点(通常是相对于某个基地点的偏移量,或者是一个绝对地点)来实现。
5、对照分析
(1)函数入口和栈帧设置:
push %rbp 和 mov %rsp, %rbp 用于设置新的栈帧。
sub $0x20, %rsp 为局部变量分配栈空间。
(2)参数传递和函数调用:
mov %edi, -0x14(%rbp) 和 mov %rsi, -0x20(%rbp) 生存函数参数(如果有的话)。
callq 指令用于调用函数,如 puts、exit、printf 等。这些调用通常通过动态链接器(dynamic linker)剖析为现实的函数地点。
(3)条件跳转和循环:
cmpl 指令用于比力两个值。
je 和 jle 指令用于条件跳转,根据比力结果决定是否跳转。
jmp 指令用于无条件跳转,形成循环结构。
(4)返回值处理惩罚:
mov $0x1, %edi 设置 exit 函数的返回值。
callq 调用函数后,返回值通常生存在 eax 或 rax 寄存器中。
(5)动态链接和重定位:
反汇编代码中的 R_X86_64_PLT32 和 R_X86_64_PC32 等重定位条目表示需要动态链接器在运行时剖析的地点。
4.5 本章小结
本章深入阐述了汇编过程的具体细节。汇编器将汇编语言转换成呆板语言,并生成了名为hello.o的可重定位目标文件,为后续的链接阶段奠定了坚实的根本。通过对比hello.s汇编代码与hello.o反汇编代码之间的差异,我们得以更深刻地洞察汇编语言是如何一步步转化为呆板语言的,以及这一转化过程中为链接所做的各项准备。此外,本章还详细剖析了可重定位目标文件的ELF格式,尤其聚焦于其中的重定位条目,这些条目在链接过程中饰演着至关紧张的角色。为了加深理解,我们对hello.o文件进行了反汇编处理惩罚,并将得到的disas_hello.s文件与之宿世成的hello.s文件进行了对比。通过这一章的使命,使我对汇编有了更加深刻的理解。
第5章 链接
5.1 链接的概念与作用
概念:
链接是将多个目标文件(如hello.o)以及所需的库文件合并成一个可实行文件(如hello)的过程。这个过程由链接器(linker)自动实行,在当代盘算机系统中,链接通常是在编译过程的后期自动进行的。链接的主要目的是实当代码的分离编译,即将大型步调分解成多个更小的、更好管理的模块,每个模块可以独立编译,最后通过链接器合并成一个可实行文件。
作用:
链接使得步调员可以将大型步调分解成多个更小的模块,每个模块可以独立编译和测试。这不仅提高了编程效率,还有助于代码的模块化和重用。链接器可以或许将步调所需的库文件合并到可实行文件中。这些库文件包罗了步调运行时所需的函数和数据结构。同时,链接器在链接过程中可以进行一些优化操作,如删除未使用的代码和数据、合并重复的代码段等。这些优化操作有助于提高步调的运行效率和淘汰内存占用。
5.2 在Ubuntu下链接的命令
命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /x86_64-linux-gnu/crt1.o /x86_64-linux-gnu/crti.o hello.o /x86_64-linux-gnu/libc.so /x86_64-linux-gnu/crtn.o
图 8 链接命令
5.3 可实行目标文件hello的格式
命令:readelf -a hello > hello1.elf
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
种别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
范例: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地点: 0x0
步调头起点: 0 (bytes into file)
Start of section headers: 1264 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
节头:
[号] 名称 范例 地点 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000009d 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000003a0
00000000000000c0 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000dd
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000dd
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000e0
0000000000000040 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 00000120
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000014c
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 00000150
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000170
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000460
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 000001a8
00000000000001b0 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000358
0000000000000048 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000478
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
There are no section groups in this file.
本文件中没有步调头。
There is no dynamic section in this file.
重定位节 '.rela.text' at offset 0x3a0 contains 8 entries:
偏移量 信息 范例 符号值 符号名称 + 加数
00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000021 000c00000004 R_X86_64_PLT32 0000000000000000 puts - 4
00000000002b 000d00000004 R_X86_64_PLT32 0000000000000000 exit - 4
00000000005f 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 2c
000000000069 000e00000004 R_X86_64_PLT32 0000000000000000 printf - 4
00000000007c 000f00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
000000000083 001000000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000092 001100000004 R_X86_64_PLT32 0000000000000000 getchar - 4
重定位节 '.rela.eh_frame' at offset 0x460 contains 1 entry:
偏移量 信息 范例 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
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 157 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
No version information found in this file.
Displaying notes found in: .note.gnu.property
全部者 Data size Description
GNU 0x00000010 NT_GNU_PROPERTY_TYPE_0
Properties: x86 feature: IBT, SHSTK
5.4 hello的捏造地点空间
使用EDB调试器加载名为"hello"的步调后,通过其Data Dump窗口,我们可以清晰地观察到该步调在捏造地点空间中的加载状态。深入探究,ELF格式文件中的Program Headers部分饰演着至关紧张的角色,它们为链接器提供了详尽的运行时加载引导,并包罗了动态链接所必需的信息。每个Program Header表项都全面形貌了步调的不同段在捏造地点空间(以及在某些情况下物理地点空间)中的布局、权限等关键信息。具体来说,"hello"步调包罗了形貌步调头表自身位置的PHDR、指明动态链接解释器路径的INTERP、包罗主要代码和数据的LOAD段、存储动态链接和重定位信息的DYNAMIC段、记录额外步调信息的NOTE段、标记堆栈是否可实行的GNU_STACK段,以及提高步调安全性的GNU_RELRO段。这些组成部分相互协作,共同确保了"hello"步调可以或许正确、安全地加载和运行。
图 9 edb中Data Dump视图
PHDR负责生存步调头表的信息,它是ELF文件中不可或缺的一部分。INTERP则指定了一个特定的解释器,该步调在已经从可实行文件映射到内存之后必须被调用,以确保动态链接的顺利进行。LOAD表示一个关键的段,它需要将包罗常量数据、步调目标代码等内容的二进制文件部分映射到捏造地点空间中。DYNAMIC段则专门用于存储动态链接器所需的紧张信息。此外,NOTE段生存了与步调相干的辅助信息,这些信息可能对调试或特定功能的实现至关紧张。GNU_STACK是一个权限标志,它明确指出了栈是否具备可实行权限,这一特性对于提升步调的安全性具有紧张意义。最后,GNU_RELRO则指定了在重定位操作完成之后,哪些内存区域应当被设置为只读状态,这同样是为了增强步调的安全性和稳固性。
图 10 Linux 历程的捏造地点空间
5.5 链接的重定位过程分析
命令:objdump -d -r hello > hello_objdump.s
Hello与hello.o的不同:
1、文件范例不同,hello.o是一个目标文件(object file),通常是由编译器生成的,包罗了步调的呆板指令,但尚未经过链接器的处理惩罚。hello是一个可实行文件(executable file),它是由链接器将目标文件、库文件等链接在一起后生成的,可以直接被操作系统加载实行。
2、hello.o包罗了步调的呆板指令、符号表(用于记录步调中界说的符号和引用的外部符号)、重定位表(用于记录需要重定位的符号和位置)等。hello包罗了完备的步调指令、数据段、堆栈段、符号表(但通常被优化或移除,除非使用了特定的链接器选项)、动态链接信息(如果使用了动态链接库)等。
3、重定位不同,hello.o中的指令和数据可能包罗了对其他符号的引用,这些引用在链接前是未剖析的(即重定位的)。hello中的指令和数据已经过链接器的处理惩罚,全部重定位的符号都已被剖析并修正为正确的地点。
链接的过程:
首先是符号剖析,链接器会扫描全部输入的目标文件和库文件,收集并剖析符号表。对于每个未界说的符号,链接器会尝试在其他目标文件或库文件中找到其界说。之后开始重定位,链接器会根据重定位表,将目标文件中的指令和数据中的符号引用修正为正确的内存地点。这包括修正函数调用、变量访问等指令中的地点。链接器会将全部输入文件的代码段、数据段等合并到终极的可实行文件中,并分配得当的内存地点。经过上述处理惩罚后,链接器会生成一个完备的可实行文件,该文件可以直接被操作系统加载实行。
hello.o的重定位项目与hello中的重定位处理惩罚
在hello.o中,重定位项目记录了需要重定位的符号和位置。这些项目通常包括符号的名称、范例、所在段、偏移量等信息。在链接过程中,链接器会读取这些重定位项目,并根据符号剖析的结果,将每个符号引用修正为正确的内存地点。比方,如果hello.o中有一条指令引用了外部函数printf,那么链接器会在其他目标文件或库文件中找到printf函数的界说,并将其地点修正到该指令中。在hello可实行文件中,这些重定位的符号引用已经被修正为正确的内存地点,因此当步调实行时,指令可以直接访问到正确的函数和数据。
5.6 hello的实行流程
子函数名和地点(后6位)
401000 <_init>
401020 <.plt>
401030 <puts@plt>
401040 <printf@plt>
401050 <getchar@plt>
401060 <atoi@plt>
401070 <exit@plt>
401080 <sleep@plt>
401090 <_start>
4010c0 <_dl_relocate_static_pie>
4010c1 <main>
401150 <__libc_csu_init>
4011b0 <__libc_csu_fini>
4011b4 <_fini>
图11 edb运行步调
5.7 Hello的动态链接分析
在elf文件中可以找到:
[18] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[19] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
0x0000000000000003 (PLTGOT) 0x404000
进入edb检察:
图 12 edb实行init之前的地点
图 13 edb在实行init之后的地点
对于变量而言,我们依赖于代码段和数据段在内存中的相对位置保持不变的原则,来盘算并确定其正确的访问地点。而对于库函数的调用,情况则稍显复杂,需要过程链接表和全局偏移表的协同工作。在步调启动的初期,PLT 中存储的是一系列特定的代码片段,这些代码片段会负责跳转到 GOT 中所指示的地点去实行。此时,GOT 中存放的通常是 PLT 中第二条指令的地点,形成一个初始的跳转循环。当链接器在步调加载时或初次调用某个库函数时,它会修改 GOT 中对应的条目,将其更新为库函数在内存中的现实地点。因此,在后续的库函数调用中,当再次实行 PLT 中的代码时,它会跳转到 GOT 中已经更新为正确地点的位置,从而可以或许正确地调用到库函数。这样,PLT 就能准确地跳转到库函数所在的内存区域,完成函数的调用。
5.8 本章小结
本章内容主要让我们深入复习了在Linux环境下链接的复杂过程。通过细致观察hello步调的捏造地点空间布局,并对比hello可实行文件与hello.o目标文件的反汇编代码,我们得以更深刻地理解链接过程中的重定位机制。
第6章 hello历程管理
6.1 历程的概念与作用
历程的概念:
历程是实行中步调的抽象。
历程的作用:
操作系统通过历程来管理各种资源,如CPU时间、内存空间、I/O装备等。每个历程都被分配一定的资源,并在其生命周期内独立地使用这些资源。历程是步调实行的载体。它使得步调可以或许在操作系统的控制下运行。当历程被创建时,它会加载步调代码和数据,并设置必要的实行环境。然后,操作系统会调度该历程运行,实行其步调代码。同时,历程提供了并发实行的机制。通过创建多个历程,操作系统可以支持步调的并发实行。这提高了系统的吞吐量和相应时间,使得多个使命可以同时进行。每个历程都有自己的地点空间和资源,这使得历程之间可以相互隔离,制止了因一个历程的错误或崩溃而影响其他历程的运行。这提高了系统的稳固性和安全性。
6.2 简述壳Shell-bash的作用与处理惩罚流程
作用:解释命令,连接用户和操作系统以及内核
流程:
shell先分词,判断命令是否为内部命令,如果不是,则探求可实行文件进行实行,重复这个流程:
- Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成隔断符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
- 步调块tokens被处理惩罚,检检察他们是否是shell中所引用到的关键字。
- 当步调块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,实行替换操作并且处理惩罚过程回到第一步重新分割步调块tokens。
- Shell对~符号进行替换。
- Shell对全部前面带有$符号的变量进行替换。
- Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command)标记法。
- Shell盘算采用$(expression)标记的算术表达式。
- Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包罗有:SPACE , TAB 和换行符号。
- Shell实行通配符* ? [ ]的替换。
- shell把全部從處理的結果中用到的注释删除,並且按照下面的次序实行命令的检查:
- 内建的命令
- shell函数(由用户自己界说的)
- 可实行的脚本文件(需要探求文件和PATH路径)
- 在实行前的最后一步是初始化全部的输入输出重定向。
- 最后,实行命令。
6.3 Hello的fork历程创建过程
根据Shell的处理惩罚机制,当输入并实行hello命令时,如果该命令不被识别为内部指令,父历程将会通过调用fork函数来创建一个新的子历程。这个子历程在诸多方面与父历程相似,它获得了父历程用户级捏造空间的一份独立且相同的副本,这包括数据段、代码段、共享库、堆区域以及用户栈。此外,父历程已经打开的文件形貌符在子历程中同样有效,子历程可以或许对这些文件进行读写操作。然而,两者之间存在一个显著的区别,那就是它们的历程标识符(PID)是不同的。值得注意的是,fork函数的调用具有独特性:它只被触发一次,但却会返回两次结果。在父历程中,fork函数返回的是新创建的子历程的PID,而在子历程中,fork则返回0。这种机制确保了父历程和子历程可以或许各自独立地识别和处理惩罚fork操作的结果。
6.4 Hello的execve过程
execve函数负责在当前历程的上下文中加载并运行一个新的可实行目标文件,好比Hello,同时携带参数列表argv和环境变量列表envp。此函数的焦点作用在于替换当前历程的映像,以实行指定的新步调。仅在遇到错误情况,比方找不到Hello可实行文件时,execve才会返回调用它的步调,这与fork函数的一次调用两次返回的特性形成光显对比。
在execve乐成加载Hello步调后,它会触发一系列启动代码的实行。这些启动代码负责以下关键使命:
1、清算并重置用户区域:首先,它会删除当前历程(从父历程继承而来)中已存在的用户空间区域,为新步调腾出空间。
2、映射私有区域:接着,为Hello步调的代码段、数据段、.bss段以及栈区域创建新的内存区域结构。这些区域都是私有的,采用写时复制(Copy-On-Write, COW)机制,以确保历程间的独立性。
3、映射共享区域:如果Hello步调与标准C库libc.so等对象文件动态链接,那么这些共享对象也会被映射到用户捏造地点空间的共享区域内,以便多个历程可以共享同一份代码和数据(在得当的权限控制下)。
4、设置步调计数器:最后,execve会设置当前历程的上下文,特别是步调计数器(Program Counter, PC),使其指向新步调代码区域的入口点。这样,当控制权转移给新步调时,它将从指定的入口点开始实行。
6.5 Hello的历程实行
逻辑控制流:
一系列步调计数器 PC 的值的序列叫做逻辑控制流。由于历程是轮流使用处理惩罚器的,同一个处理惩罚器每个历程实行它的流的一部分后被抢占,然后轮到其他历程。
用户模式和内核模式:
处理惩罚器使用一个寄存器提供两种模式的区分。用户模式的历程不答应实行特殊指令,不答应直接引用地点空间中内核区的代码和数据;内核模式历程可以实行指令会合的任何命令,并且可以访问系统中的任何内存位置。
上下文:
上下文就是内核重新启动一个被抢占的历程所需要规复的原来的状态,由寄存器、步调计数器、用户栈、内核栈和内核数据结构等对象的值构成。初始时,控制流再hello内,处于用户模式。调用系统函数sleep后,进入内核态,此时间片停止。2s后,发送停止信号,转回用户模式,继续实行指令。
调度的过程:
在历程实行的某些关键时刻,内核有权决定停止当前正在运行的历程,并转而重新启动一个之前被暂停的历程。这一决策过程被称为调度,由内核内部专门负责的调度器代码来实行。当内核选定一个新的历程以运行时,我们称之为对该历程进行了调度。一旦内核调度了一个新的历程,它会通过上下文切换机制,停止当前历程的实行,并将控制权安稳地转移给新的历程。
用户态与焦点态转换:
为了能让处理惩罚器安全运行,不至于破坏操作系统,必然需要先知应用步调可实行指令所能访问的地点空间范围。因此,就存在了用户态与焦点态的划分,焦点态拥有最高的访问权限,处理惩罚器以一个寄存器当做模式位来形貌当前历程的特权。历程只有故障、停止或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理惩罚
正常运行状态:
图 15 正常运行状态
异常范例:
种别
| 原因
| 异步/同步
| 返回行为
| 停止
| 来自I/O装备的信号
| 异步
| 总是返回到下一条指令
| 陷阱
| 故意的异常
| 同步
| 总是返回到下一条指令
| 故障
| 潜伏可规复的错误
| 同步
| 可能返回到当前指令
| 终止
| 不可规复的错误
| 同步
| 不会返回
|
处理惩罚方式:
图 16 停止处理惩罚方式
图 17 陷阱处理惩罚方式
图 18 故障处理惩罚方式
图 19 终止处理惩罚方式
按下Ctrl+Z:历程收到 SIGSTP 信号, hello 历程挂起。用ps检察其历程PID,可以发现hello的PID是2681;再用jobs检察此时hello的后台 job号是1,调用 fg 1将其调回前台。
图 20 按下Ctrl+Z运行状态
Ctrl+C:历程收到 SIGINT 信号,竣事 hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底竣事。
图 21 按下Ctrl+C运行状态
中途乱按:只是将屏幕的输入缓存到缓冲区。乱码被认为是命令。
图 22 中途乱按运行状态
Kill命令:挂起的历程被终止,在ps中无法查到到其PID。
图 23 输入kill命令运行状态
6.7本章小结
在本章中,我们深入相识了hello历程从创建、加载到终止的完备实行过程,这一过程主要通过键盘输入进行触发和控制。步调,作为指令、数据及其构造结构的集合体,而历程则是这些步调在操作系统中的现实运行实体。简而言之,历程是步调在实行状态下的具体表现。在hello历程的运行期间,内核饰演着至关紧张的管理角色,它负责决定何时对hello历程进行上下文切换,以确保系统资源的有效分配和多个历程间的平滑调度。同时,在hello历程的运行过程中,一旦接收到各种异常信号,系统会立刻触发相应的异常处理惩罚步调。这些处理惩罚步调针对不同范例的信号,实行特定的代码逻辑,每种信号都有其独特的处理惩罚流程和机制。因此,面对不同的异常信号,hello历程会展现出不同的相应和处理惩罚结果。
第7章 hello的存储管理
7.1 hello的存储器地点空间
逻辑地点:逻辑地点(Logical Address)是指由步调hello产生的与段相干的偏移地点部分(hello.o)。
线性地点:线性地点(Linear Address)是逻辑地点到物理地点变更之间的中间层。步调hello的代码会产生逻辑地点,或者说是(即hello步调)段中的偏移地点,它加上相应段的基地点就生成了一个线性地点。
捏造地点:偶然我们也把逻辑地点称为捏造地点。因为与捏造内存空间的概念类似,逻辑地点也是与现实物理内存容量无关的,是hello中的捏造地点。
物理地点:物理地点(Physical Address)是指出现在CPU外部地点总线上的寻址物理内存的地点信号,是地点变更的终极结果地点。如果启用了分页机制,那么hello的线性地点会使用页目次和页表中的项变更成hello的物理地点;如果没有启用分页机制,那么hello的线性地点就直接成为物理地点了。
7.2 Intel逻辑地点到线性地点的变更-段式管理
逻辑地点由段标识符(即段选择符)和段内偏移量构成。段选择符是一个16位的字段,其中前13位作为索引号,用于在段形貌符表中定位具体的段形貌符。该形貌符详细形貌了段的属性,而我们主要关注的是其中的Base字段,它指明确段的起始线性地点。
段形貌符分为全局和局部两类,分别存储在全局段形貌符表(GDT)和局部段形貌符表(LDT)中。GDT和LDT的地点及大小信息分别由CPU的gdtr和ldtr寄存器生存。
给定一个逻辑地点,首先通过段选择符的T1位判断其指向的是GDT还是LDT中的段。然后,结合相应的寄存器信息,我们可以定位到对应的段形貌符表。接着,使用段选择符的前13位索引号,在表中查找对应的段形貌符,获取其Base字段值。最后,将Base字段值与段内偏移量相加,即可得到所需的线性地点。
7.3 Hello的线性地点到物理地点的变更-页式管理
页式管理是一种高效的内存空间存储管理技能,它主要分为静态页式管理和动态页式管理两种形式。在这种管理方式下,各个历程的捏造空间被匀称地划分成多个长度相等的页(page)。相应地,内存空间也被按照页的大小切割成页面(page frame)。为了创建捏造地点与内存地点之间的映射关系,系统构建了一个页表,该表实现了页式捏造地点与内存地点之间的一一对应。此外,借助专门的硬件地点变更机构,页式管理可以或许有效地解决离散地点变更的难题。更进一步地,页式管理通过采用请求调页或预调页技能,实现了内外存储器的同一和高效管理。
7.4 TLB与四级页表支持下的VA到PA的变更
每当CPU生成一个捏造地点时,内存管理单元(MMU)便需查阅一个页表条目(PTE),以完成从捏造地点到物理地点的翻译过程。在最倒霉的情况下,这一操作需要从内存中读取数据,其延迟可能高达几十到几百个时钟周期。然而,如果PTE恰好被缓存在L1缓存中,那么这一开销将显著淘汰至仅1或2个时钟周期。为了进一步优化性能,许多系统致力于消除即便是这样微小的开销,因此在MMU中内置了一个小型缓存,专门用于存储PTE,这个缓存被称为翻译后备缓冲器(TLB)。
多级页表:
将捏造地点的VPN划分为相等大小的不同的部分,每个部分用于探求由上一级确定的页表基址对应的页表条目。
图 24 使用k级页表进行翻译
剖析VA,使用前m位vpn1探求一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA
7.5 三级Cache支持下的物理内存访问
当CPU发出一条捏造地点请求后,MMU(内存管理单元)负责将其转换为物理地点PA。接着,根据cache的组数和大小要求,物理地点PA被分解为三个部分:CT(标记位)、CS(组号)和CO(偏移量)。使用CS,系统可以或许定位到cache中相应的组。在该组内,系统会逐一检查每个cache line,验证其标记位是否有效,并比力CT是否与目标地点的CT相匹配。若找到匹配的cache line,即表示掷中,系统会立刻返回所需数据给CPU。若未掷中,则系统会依次在L2 cache、L3 cache以及主存中进行查找,直到找到匹配的数据为止。一旦数据被找到并掷中,它会被传递给CPU,同时,各级cache中的相干cache line也会被更新以反映这一新数据。若cache已满,系统会采用换入换出策略来管理cache line的替换。
。
图 25 3级Cache
7.6 hello历程fork时的内存映射
调用fork函数时,内核为新历程构建必要的数据结构,分配唯一PID,并复制当前历程的捏造内存布局,包括mm_struct、区域结构和页表。这些复制的内容最初被标记为只读和写时复制。新历程初始时的捏造内存与调用fork时同等。随后,任一历程的写操作将触发写时复制机制,创建新页面,确保各历程拥有独立的地点空间。
7.7 hello历程execve时的内存映射
在bash历程中实行execve("hello", NULL, NULL)调用时,会发生以下过程:
- execve函数负责在当前历程中加载并运行可实行文件hello中的步调,此操作会替换当前bash历程中的步调。
加载并运行hello的具体步调如下:
- 系统会删除当前历程中已存在的用户区域。
- 为hello步调映射新的私有区域,这些区域仅对当前历程可见。
- 映射共享区域,这些区域可能被多个历程共享,如动态链接库等。
- execve函数会设置步调计数器(PC),即指向hello步调代码区域的入口点。
execve完成的最后一项关键使命是设置当前历程的上下文,特别是将步调计数器指向hello步调的起始实行点。这样,当下一次调度该历程时,它将从这一入口点开始实行。Linux操作系统会根据现实需求,动态地加载所需的代码和数据页面。
7.8 缺页故障与缺页停止处理惩罚
页面掷中完全是由硬件完成的,而处理惩罚缺页是由硬件和操作系统内核协作完成的:
图 26 缺页停止处理惩罚
整体的处理惩罚流程:
- 处理惩罚器生成一个捏造地点,并将它传送给MMU
- MMU生成PTE地点,并从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- PTE中的有效位是0,以是MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理惩罚步调。
- 缺页处理惩罚步调确认出物理内存中的捐躯页,如果这个页已经被修改了,则把它换到磁盘。
- 缺页处理惩罚步调页面调入新的页面,并更新内存中的PTE
- 缺页处理惩罚步调返回到原来的历程,再次实行导致缺页的命令。CPU将引起缺页的捏造地点重新发送给MMU。因为捏造页面已经换存在物理内存中,以是就会掷中。
7.9动态存储分配管理
动态存储分配通过动态内存分配器管理历程的堆区域,该区域由不同大小的块组成,分为已分配和空闲两种状态。已分配块供应用步调使用,而空闲块等待分配。动态内存分配主要有两种方法:
1、带边界标签的隐式空闲链表分配器:每个块包罗头部(含大小字段)、有效载荷及可能的尾部。空闲块通过头部大小字段隐含连接,分配器遍历堆查找足够大的空闲块进行分配,采用初次适配、下一次适配或最佳适配策略,并可分割空闲块淘汰内部碎片。释放时,使用边界标记合并空闲块。
2、显式空闲链表管理:空闲块构造成显式数据结构,如双向链表,块内包罗前驱和后继指针。链表可按后进先出或地点次序维护,释放块时需搜索定位前驱以维护链表结构。
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。
我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等
8.2 简述Unix IO接口及其函数
Unix IO接口提供了一系列函数,用于对文件进行打开、读写、定位及关闭等操作。当文件被打开时,内核会返回一个非负整数的文件形貌符,该形貌符成为后续全部文件操作的唯一标识。每个由Linux shell创建的历程在初始时都会拥有三个已打开的文件:标准输入(文件形貌符0)、标准输出(文件形貌符1)和标准错误输出(文件形貌符2),这些形貌符也可通过<unistd.h>头文件中界说的常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO来引用。
对于文件的读写操作,Unix IO接口提供了read和write函数。read函数负责从文件的当前位置开始,将指定数量的字节复制到内存中;而write函数则实行相反的操作,将内存中的数据写入到文件的当前位置。文件的当前位置由文件偏移量确定,可以通过lseek函数来修改。lseek函数答应应用步调将文件的当前位置设置为指定的偏移量k。
在文件操作完成后,应使用close函数来关闭文件。这个操作会通知内核释放与文件打开时创建的数据结构相干的资源,并将文件形貌符回收到形貌符池中以供后续使用。
- open函数:用于打开或创建文件,并可以设置文件的属性及用户的权限等参数。乐成时返回文件形貌符,失败时返回-1。
- close函数:用于关闭一个已打开的文件。乐成时返回0,出错时返回-1。
- read函数:从文件的当前位置开始读取指定数量的字节到内存中。返回值为现实读取的字节数(可能小于请求的数量),当到达文件末尾时返回0,出错时返回-1。
- write函数:将内存中的数据写入到文件的当前位置。返回值为现实写入的字节数(可能小于请求的数量),出错时返回-1。
- lseek函数:用于修改文件的当前位置(文件指针)。乐成时返回新的文件偏移量(当前位置),失败时返回-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);
}
}
vsprintf函数负责将提供的参数按照指定的格式进行格式化,并将格式化后的内容存储到buf缓冲区中,随后返回已格式化字符串的长度。接着,write函数会将buf中的指定数量(设为i)的元素(通常是字符)输出到终端装备上。从vsprintf生成格式化信息,到通过write系统调用进行输出,这一过程通常涉及到底层的系统调用机制,如通过停止指令int 0x80或syscall来触发。在字符显示的过程中,驱动子步调饰演着关键角色。它首先将ASCII字符转换为对应的字模信息,这些信息通常存储在字模库中。然后,驱动子步调将这些字模信息映射到视频内存(VRAM)中,VRAM负责存储屏幕上每个像素点的RGB颜色信息。终极,显示芯片会根据预设的革新频率,逐行读取VRAM中的数据,并通过信号线将这些颜色信息传输到液晶显示器上,从而呈现出我们看到的图像和字符。
8.4 getchar的实现分析
getchar函数具有一个int范例的返回值,用于从标准输入(通常是键盘)读取字符。当步调调用getchar时,它会等待用户按键输入。用户输入的字符会被存储在键盘缓冲区中,这个存储过程会一直连续到用户按下回车键为止,此时回车键字符也会被参加到缓冲区中。
在用户按下回车键后,getchar函数开始从stdio流中逐个读取字符。它返回的是用户输入的第一个字符的ASCII码值。如果读取过程中出现错误,getchar会返回-1,并且会将用户输入的字符回显到屏幕上。如果用户在按下回车键之前输入了多个字符,这些额外的字符会保留在键盘缓冲区中,等待后续的getchar调用进行读取。这意味着,在读取完缓冲区中的全部字符之前,后续的getchar调用不会再次等待用户按键输入,而是直接读取缓冲区中已存在的字符。
关于异步异常和键盘停止的处理惩罚,当键盘产生停止时,键盘停止处理惩罚子步调会被激活。这个子步调负责接收按键的扫描码,并将其转换成ASCII码,然后生存到系统的键盘缓冲区中。
getchar等函数在内部会调用read系统函数,通过系统调用来读取键盘缓冲区中的按键ASCII码。这个读取过程会一直连续,直到接收到回车键为止,此时getchar才会返回读取到的字符。
8.5本章小结
本章主要先容了linux的I/O装备的根本概念以及一些管理方法,同时还先容了Unix I/O接口及相干函数,最后主要分析了printf函数和getchar函数的工作过程。
结论
Hello的一生布满了多样的变化历程:
1、初始的hello.c文件经过预编译阶段,被扩展并生成了hello.i文本文件,这一步调为后续的编译过程奠定了根本。
2、hello.i文件随后进入编译阶段,被转换成汇编代码,并生存为hello.s汇编文件。这一步调将高级语言转换为呆板可读的低级指令。
3、接着,hello.s汇编文件经过汇编器的处理惩罚,被转换成二进制格式的可重定位目标文件hello.o。这一步调将汇编指令转换为呆板码,并准备好与其他目标文件进行链接。
4、hello.o文件与其他必要的库文件一起经过链接器的处理惩罚,终极生成了可实行文件hello。这一步调将各个目标文件组合成一个完备的步调,并准备好在目标平台上运行。
5、当bash历程调用fork函数时,会生成一个子历程。随后,execve函数在子历程的上下文中加载并运行新步调hello。这一步调实现了步调的动态加载和实行。
6、在hello的变化过程中,会涉及到多种地点的转换,但终极我们真正关注的是PA(物理地点),因为这是步调在内存中现实占用的位置。
7、当hello步调运行时,它会调用一些如printf之类的函数,这些函数与Linux I/O装备的模拟化紧密相干,实现了数据的输入和输出功能。
8、当hello步调运行竣事后,它会被shell父历程回收,内核会收回为其创建的全部资源,包括内存、文件形貌符等。
《深入理解盘算机系统》作为盘算机系统领域的经典之作,详细先容了盘算机系统的根本概念和原理。从底层的内存数据表示、流水线指令的构成,到高级的捏造存储器、编译系统、动态加载库以及用户应用,书中内容涵盖了盘算机系统的各个方面。通过大量的现实操作和案例分析,读者可以深入理解步调实行的方式,并学会如何改历步调的实行效率。此书以步调员的视角出发,全面而深入地解说了处理惩罚器、编译器、操作系统和网络环境等关键领域,是盘算机系统领域
权威之作,值得每一位CSer深入研读。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |