大作业
题 目 程序人生-Hello’s P2P
专 业 计算机类
学 号 120L022115
班 级 2003007
学 生 王炳轩
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
摘 要
本文主要讲述了hello的一生:从hello程序的制作:源代码hello.c、预处理hello.i、汇编hello.s、编译hello.o、链接可执行目标文件hello,以及hello程序在linux系统下的整个生命周期:对hello进行执行,最终hello程序结束运行中系统为hello所做的事情。
通过使用Linux操作系统,对hello进行执行,用gcc、edb、readelf、objdump等工具进行实验。分析解读了Linux下的hello从C代码变成可执行程序再到程序执行的整个过程。通过对hello.c一生的总结,加深了我们对于计算机系统课程的理解,深入了解汇编、ELF文件、shell、虚拟内存、动态内存分配以及IO管理的相关知识。
关键词:计算机系统;预处理;编译;汇编;链接;内存;CPU;
目 录
第1章 概述... - 4 -
1.1 Hello简介... - 4 -
1.2 环境与工具... - 4 -
1.3 中间结果... - 5 -
1.4 本章小结... - 5 -
第2章 预处理... - 6 -
2.1 预处理的概念与作用... - 6 -
2.2在Ubuntu下预处理的命令... - 6 -
2.3 Hello的预处理结果解析... - 7 -
2.4 本章小结... - 7 -
第3章 编译... - 8 -
3.1 编译的概念与作用... - 8 -
3.2 在Ubuntu下编译的命令... - 8 -
3.3 Hello的编译结果解析... - 9 -
3.4 本章小结... - 10 -
第4章 汇编... - 12 -
4.1 汇编的概念与作用... - 12 -
4.2 在Ubuntu下汇编的命令... - 12 -
4.3 可重定位目标elf格式... - 12 -
4.4 Hello.o的结果解析... - 14 -
4.5 本章小结... - 17 -
第5章 链接... - 18 -
5.1 链接的概念与作用... - 18 -
5.1.1 链接的概念... - 18 -
5.1.2 链接的作用... - 18 -
5.2 在Ubuntu下链接的命令... - 18 -
5.3 可执行目标文件hello的格式... - 18 -
5.4 hello的虚拟地址空间... - 20 -
5.5 链接的重定位过程分析... - 22 -
5.6 hello的执行流程... - 23 -
5.8 本章小结... - 26 -
第6章 hello进程管理... - 27 -
6.1 进程的概念与作用... - 27 -
6.2 简述壳Shell-bash的作用与处理流程... - 27 -
6.3 Hello的fork进程创建过程... - 27 -
6.4 Hello的execve过程... - 28 -
6.5 Hello的进程执行... - 28 -
6.6 hello的异常与信号处理... - 28 -
6.7 本章小结... - 31 -
第7章 hello的存储管理... - 32 -
7.1 hello的存储器地址空间... - 32 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 32 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 33 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 34 -
7.5 三级Cache支持下的物理内存访问... - 36 -
7.6 hello进程fork时的内存映射... - 37 -
7.7 hello进程execve时的内存映射... - 37 -
7.8 缺页故障与缺页中断处理... - 38 -
7.9 动态存储分配管理... - 38 -
7.10 本章小结... - 40 -
第8章 hello的IO管理... - 41 -
8.1 Linux的IO设备管理方法... - 41 -
8.2 简述Unix IO接口及其函数... - 41 -
8.3 printf的实现分析... - 42 -
8.4 getchar的实现分析... - 44 -
8.5 本章小结... - 45 -
结论... - 45 -
附件... - 45 -
参考文献... - 47 -
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program To Process
在Linux系统中,程序(Program)源代码hello.c经过预处理(cpp)、编译(ccl)、汇编(as)、链接(ld)最终输出可执行目标程序hello。
在shell(Terminal bash)中输入命令行./hello后,shell调用OS(进程管理)函数fork(),配合execve(),产生了hello的相应(子)进程(Process)。
此外,操作系统通过mmap、CPU分时等操作,使程序取指、译码、流水线执行;OS(存储管理)与MMU还通过VA到PA、TLB、4级页表、3级Cache,Pagefile等等各显神通加速程序的执行;IO管理与信号处理帮助程序在键盘、主板、显卡、屏幕上传输数据、展示数据。
020:From Zero To Zero
在Linux操作系统中,在shell进程fork一个子进程后,在子进程中调用execve函数装载hello程序,将相关可执行文件中的段(segment)映射入虚拟内存,操作系统为这个进程分时间片。当该进程的时间片到达时,操作系统设置CPU上下文环境,并跳到程序开始处。当指令将“Hello World\n”字符串中的字节从主存经一系列cache复制到寄存器文件,再从寄存器文件中经I/O管理复制到显示设备,最终显示在屏幕上。程序执行完毕,shell回收这个子进程,完成hello相关内存的释放。hello这个程序,在内存中从无到无,即是020。
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:Surface Go 3(x86-64、Intel Pentium G6500-Y @1.1GHz、8GB DRAM、128GB SSD)
软件环境:Windows 11、Windows Subsystem for Linux、Ubuntu 20.04
开发调试工具:Virtual Studio 2019、Dev C++、Code::Blocks、gcc、gdb、edb、objdump、vi/vim、as、ld、readelf、winHex
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 |
作用 |
hello.c |
程序源代码 |
hello.i |
hello.c经预处理之后得到的程序文件 |
hello.s |
hello.i文件经编译后得到的汇编语言源代码文件 |
hello.o |
hello.s文件经汇编后得到的可重定位目标文件 |
hello |
hello.o文件经过链接后得到的可执行目标文件 |
hello.o.objdump.txt |
hello.o的反汇编代码 |
hello.objdump.txt |
hello的反汇编代码 |
本章主要介绍了hello程序的P2P,020过程,以及进行实验时的软硬件环境、开发与调试工具和在本论文中生成的中间结果文件。
2.1.1预处理的概念
预处理是在编译步骤之前进行的处理。预处理的主要任务是执行预处理指令。预处理指令包括条件包含(#ifdef、#else、#endif等)、源文件包含(#include)、宏替换(#define)、行控制、抛错、杂注和空指令。每个预处理指令以预处理记号“#”开头,后面跟着一系列其他预处理记号,最后结束于“#”后面的第一个换行符。
2.1.2预处理的作用
通过预处理生成的预处理文件(hello.i)是一个可读但不包含任何宏定义的文件。预处理的具体作用表现为:
1. 删除“#define”并展开所定义的宏。
2. 插入头文件到“#include”处,可以递归方式进行处理。
3. 处理所有条件包含指令,如“#if”、“#ifdef”、“#endif”等。
4. 添加行号和文件名标识,以便编译器产生调试用的行号信息。
5. 删除所有的注释“//”和“/* */”。
6. 保留所有的#pragma编译指令。
gcc -E hello.c -o hello.i 或 cpp hello.c > hello.i
在预处理的过程当中,预处理器依次读取所include的头文件中所使用的代码段,将其插入到hello.i 文件当中,从文件头开始依次是头文件stdio.h、unistd.h、stdlib.h的展开;同时对于这些文件当中出现的宏定义(#define)也进行相应的宏替换、对于其中的条件编译进行处理,并且删除了源文件当中全部的注释。
本章介绍了预处理的概念及作用、通过运行命令展示预处理的结果并进行解析。
3.1.1编译的概念
编译是指利用编译程序从语言编写的源程序代码产生汇编程序代码的过程。编译程序把一个源程序代码翻译成汇编语言程序代码的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,则给出提示信息。
编译器的构建流程主要分为 3 个步骤:
1. 词法分析器,用于将字符串转化成内部的表示结构。
2. 语法分析器,将词法分析得到的标记流(token)生成一棵语法树。
3. 目标代码的生成,将语法树转化成目标代码。
3.1.2编译的作用
把预处理完的程序代码进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。例如,编译器(ccl)将hello.i翻译成一个汇编语言程序hello.s。
gcc -S hello.i -o hello.s
将hello.c翻译为hello.s,按C语句逐行对应如下表。其中使用【】包裹的语句为该行复杂语句的子语句。
汇编代码 |
C代码 |
.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" |
char** LC0="用法: Hello 学号 姓名 秒数!\n" |
.LC1: .string "Hello %s %s\n" |
char** LC1="Hello %s %s\n" |
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) |
int main(int argc,char *argv[]){ int i; 【argc】 【argv[]】 |
cmpl $4, -20(%rbp) je .L2 |
if(argc!=4){ |
leaq .LC0(%rip), %rdi call puts@PLT |
printf("用法: Hello 学号 姓名 秒数!\n"); |
movl $1, %edi call exit@PLT |
exit(1); } |
.L2: movl $0, -4(%rbp) jmp .L3 .L3: cmpl $7, -4(%rbp) jle .L4 .L4: ……… addl $1, -4(%rbp) |
【i=0;】 【i<=7;】 for(i=0;i<8;i++){ .L4 } 【i++】 |
.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 |
【argv[2]】 【argv[1]】 【”Hello %s %s”】 printf("Hello %s %s\n", |
movq -32(%rbp), %rax addq $24, %rax movq (%rax), %rax movq %rax, %rdi call atoi@PLT movl %eax, %edi call sleep@PLT |
【argv[3]】 【atoi(argv[3])】 sleep(atoi(argv[3])); |
.L3: call getchar@PLT |
getchar(); |
movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc |
return 0; |
本章主要介绍了编译器将修改后的源文件hello.i翻译成为二进制文件hello.s的过程,并按hello.c的C代码逐行进行汇编代码的比对分析。
4.1.1 汇编的概念
汇编是指将汇编语言书写的程序翻译成为与之等价的机器语言程序的过程。汇编器将hello.s翻译成机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果输出为可重定位目标文件hello.o。
4.1.2 汇编的作用
汇编器(as)将hello.s 翻译成CPU语言指令,把这些指令打包成可重定位目标程序的格式,并且将结果保存在二进制目标文件hello.o当中。
gcc -c hello.s -o hello.o 或 as hello.s -o hello.o
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
1. ELF头
包含字大小、字节顺序、文件类型(.o,exec,.so),机器类型等。
1) Magic:魔数,指名该文件是一个ELF目标文件。第一个字节7F是个固定的数;后面的3个字节为E,L,F三个字母的ASCII形式。
2) CLASS:文件类型,这里是64位的ELF格式。
3) Data :文件中的数据是按照什么格式组织(大端或小端)的,不同处理器平台数据组织格式可能就不同,如x86平台为小端存储格式。
4) 当前ELF文件头版本号,这里版本号为1。
5) OS/ABI:指出操作系统类型,ABI 是 Application Binary Interface 的缩写。
6) ABI 版本号:当前为 0 。
7) Type :文件类型。ELF 文件有 3 种类型,一种是如上所示的 Relocatable file 可重定位目标文件,一种是可执行文件(Executable),另外一种是共享库(Shared Library)。
8) 系统架构:机器平台类型。
9) 版本:当前目标文件的版本号。
10) 入口点地址:程序的虚拟地址入口点,因为这还不是可运行的程序,故而这里为零
11) 程序头起点:与上同理,这个目标文件没有 Program Headers。
12) sections 头开始处:这里1160是十进制。
13) 标志:是一个与处理器相关联的标志,x86平台上该处为0。
14) 本头的大小:ELF 文件头的字节数。
15) 程序头大小:因为这个不是可执行程序,故此处大小为 0。
16) Number ofprogram headera:与上同理。
17) 节头大小:这里每个 section 头大小为 64个字节。
18) 节头数量:一共有多少个 section 头,这里是13个。
19) 字符串表索引节头:section头字符串表索引号,此处保存着各个sections的名字,如.data,.text,.bss等。
2.节头
包含每个节的偏移量和大小。在 Section Headers,编译阶段,.data 段会被分配一部分空间已存放数据,而 .bss 则没有,.bss 仅有的是 section headers 。链接器从 .rela.text,就可以知道哪些地方需要进行重定位(relocate) 。 .symtab 是符号表。
3.重定位节 .rela.text
当链接器把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非使用者显式地指示链接器包含这些信息。8个重定位信息分别对应.rodata节.L0,puts函数,exit函数,.rodata节.L1,printf函数,sleepsecs,sleep函数和getchar函数。
4.symtab节
一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
机器语言是一条一条与汇编语言对应的,其区别有:
操作数:机器语言反汇编后是十六进制的,而汇编语言中是十进制的。
分支跳转:机器语言是相对地址,而汇编语言是语句标号。
函数调用:机器语言是相对地址,而汇编语言是函数名。
具体比对如下:
C代码 |
|
.o反汇编代码 |
汇编代码 |
int main(int argc,char *argv[]){ int i; |
|
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) |
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) |
if(argc!=4){ |
|
13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp) 17: 74 16 je 2f |
cmpl $4, -20(%rbp) je .L2 |
printf("用法: Hello 学号 姓名 秒数!\n"); |
|
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 |
leaq .LC0(%rip), %rdi call puts@PLT |
exit(1); } |
|
25: bf 01 00 00 00 mov $0x1,%edi 2a: e8 00 00 00 00 callq 2f 2b: R_X86_64_PLT32 exit-0x4 |
movl $1, %edi call exit@PLT |
for(i=0;i<8;i++){….} |
|
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 36: eb 48 jmp 80 …… 7c: 83 45 fc 01 addl $0x1,-0x4(%rbp) 80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp) 84: 7e b2 jle 38 |
.L2: movl $0, -4(%rbp) jmp .L3 .L4: ……… addl $1, -4(%rbp) .L3: cmpl $7, -4(%rbp) jle .L4 |
printf("Hello %s %s\n",argv[1],argv[2]); |
|
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 |
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 |
sleep(atoi(argv[3])); |
|
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 |
movq -32(%rbp), %rax addq $24, %rax movq (%rax), %rax movq %rax, %rdi call atoi@PLT movl %eax, %edi call sleep@PLT |
getchar(); |
|
86: e8 00 00 00 00 callq 8b 87: R_X86_64_PLT32 getchar-0x4 |
call getchar@PLT |
return 0; } |
|
8b: b8 00 00 00 00 mov $0x0,%eax 90: c9 leaveq 91: c3 retq |
movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc |
本章介绍了在汇编的过程当中,汇编器将汇编语言转换成为机器语言。将指令打包生成可重定位目标文件的过程。并比对了可重定位目标文件机器语言与汇编语言的关系,并通过readelf指令查看相应的可重定位目标文件的内容。
链接是指将可重定位目标文件经符号解析和重定位步骤合并成可执行目标文件,这个文件可被加载到内存并执行。
使得一个项目可以被分解成许多小模块,每个模块可单独进行修改、更新,最后通过链接形成一个项目时,减少不必要的操作。
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
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
各段的入口地址、大小等基本信息如图所示。值得注意的是,入口地址与.o不同,不是从0开始了,是虚拟地址空间的地址,都是0x40xxxx。
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
Edb中显示的相关信息内容段的地址与elfheader中标识的起始地址、大小相同。
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
下表展示了objdump hello.o与hello.out的不同处。
Hello.o |
Hello.out |
未包含外部库函数代码: 调用的外部库函数使用函数名@PLT并且有额外的信息存储目标函数的地址,但实际并未在文件中,仅仅是一个引用符号。 |
包含了外部库函数代码: 有了新的init、plt、plt.sec、fini段。 其中plt.sec是hello.c调用的库函数的代码,主函数中调用库函数的跳转也变成了虚拟相对地址。 |
指令地址从0开始。 |
指令地址都被转换为虚拟内存空间地址,从0x400000开始。 |
链接的过程:把编译好的目标文件和其他的一些目标文件和库链接在一起,形成最终的可执行文件,其中包含符号解析和重定位。
重定位的过程:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并引用的库函数中的段、函数、常量等符号,并为每个符号分配运行时的地址。首先是重定位节和符号定义,链接器将hello.o及所引用的所有库文件中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的库文件和hello.o的.data节被全部合并成一个.data段。然后,链接器将运行时虚拟内存地址赋给新的聚合节,依次进行赋予定义的每个节、每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时虚拟内存地址了。然后是重定位节中的符号引用,链接器会修改hello.o中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
加载程序的过程:
ld-2.23.so_dl_start ld-2.23.so_dl_init LinkAddress_start libc-2.23.so_libc_start_main libc-2.23.so_cxa_atexit LinkAddress_libc_csu.init libc-2.23.so _setjmp
执行main函数的过程:
LinkAddress main
结束程序的过程:
libc-2.23.so exit
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
延迟绑定通过GOT和PLT实现,将过程地址的绑定推迟到下一次调用该过程时。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
GOT起始表位置为0x404000,当程序调用共享库定义的函数时,编译器无法预测这个函数的运行时地址,定义它的共享模块可以在运行时加载到任何位置。首先为该函数生成一条重定位记录,然后动态链接器在程序加载时解析。GNU编译系统使用了后期延迟技术,将过程地址的绑定延迟到第一次调用。
0x00007fca 60ade093
0x00007fca 60aed630
0x00000000 00400500
0x00007fca 6070dab0
0x00007f51 f3171430
0x00007f51 f316cc10
0x00000000 004005e7
0x00000000 004004b0
0x00000000 004004e0
在本章中主要介绍了链接的概念与作用,并且详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
进程:是一个程序在操作系统中的执行实例,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程的创建方法:在shell中,当用户运行新程序时,系统会调用fork复制与当前shell进程相同的一份资源给新程序,然后调用execve装载程序资源。
进程的回收方法:当子进程走完了自己的生命周期后,它会执行exit()系统调用,此时原来进程表中的数据会被该进程的退出码(exit code)、执行时所用的CPU时间等数据所取代,这些数据会一直保留到系统将它传递给它的父进程(必须使用wait或waitpid)为止,此段时间,子进程会成为僵尸进程。之后,子进程由父进程(创建它的进程)回收,当父进程比子进程提前结束时,由系统init进程负责回收。
Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。同时它又是一种程序设计语言。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。
shell的处理流程包括:解析指令、执行指令序列(调用系统函数)、打印执行结果信息。
shell调用系统函数fork,创建新的进程。新创建的子进程与父进程几乎相同,唯一区别就是PID不同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段,堆,共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,意味着父进程调用fork函数时,子进程可以读写父进程中打开的任何文件。
execve函数在当前进程的上下文当中加载并且运行一个新的程序。
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量envp。只有当出现错误的时候,才会返回到调用的程序。execve调用一次从不返回。
子进程调用execve装载程序数据:
Linux系统当中的每个程序都运行在一个进程上下文当中,有自己的虚拟地址空间。内核不断地进行着上下文的切换,让各个进程看似“同时”占用处理器,从而实现了进程调度。因此在Hello程序的运行的过程当中,程序被切分成一个个时间片,和其他进程交替的占用着处理器。
信号是异常的通知进程方法。Linux信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件,类似于异常和中断。从内核发送到(有时是在另一个进程的请求下)一个进程。信号类型是用小整数来标识的:{1-30}。信号中唯一的信息是它的ID和它的到达。
信号可以被进程阻塞(延迟接收处理)、忽略、捕获并自定义处理或执行默认处理行为。
内核为每一个进程维护着两个n位二进制数(n为操作系统的信号数量):blocks和pendings,这两个二进制数的每一位分别表示对第i个信号的阻塞/发生,因此,信号不能累计,每一种信号最多只能被发送1次直到被接收并清空标志位。
hello程序执行的过程中会遇到四类异常:中断,陷阱,故障和终止。
中断是来自于I/O设备的异步信号,内核调用中断处理程序进行处理,函数返回到当前指令的下一条代码进行继续执行。
陷阱是有意的异常,比如读写文件操作。是同步异常,调用陷阱处理程序进行相应的处理,处理程序后总是饭hi到当前指令的下一条指令继续进行执行。
故障是由错误情况引起的异常,当故障发生的时候,处理器将控制转移给故障处理程序。如果处理程序可以修正这个错误情况,那么将控制返回到引起故障的指令,从而重新执行这条指令。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的处理程序。
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序将控制返回给一个abort例程,该例程将会终止这个应用程序。
kill函数可以发送信号给其他的进程,包括它们自己。
Unix Shell使用job这个抽象概念来表示对一条命令行求值而创建的进程。
信号的发送方法:
被动发送信号:
系统检测到进程发生除零错误、非法内存访问等异常时。
主动发送信号:
1、在bash下通过kill命令:
给进程组发送:kill -信号ID -PGID
给单一进程发送:kill -信号ID PID(少一个负号-)
2、通过调用系统函数kill:kill(PID, SIGINT);
3、从键盘发送信号(到前台进程组):
Ctrl+c:发送SIGINT (终止)
Ctrl+z:发送SIGTSTP (挂起停止)
信号的默认处理方法:
每个信号类型都有一个预定义默认行为,是下面中的一种:
进程终止
进程终止并转储到内存
进程停止(挂起)直到被SIGCONT信号重启
进程忽略该信号
阻塞与解除阻塞信号的方法:
隐式阻塞机制
内核默认阻塞与当前正在处理信号类型相同的待处理信号。
显式阻塞和解除阻塞机制
sigprocmask函数及其辅助函数可以明确地阻塞/解除阻塞选定的信号。
辅助函数:
·sigemptyset——初始化set为空集合
·sigflllset——把每个信号都添加到set中
·sigaddset——把指定的信号signum添加到set中
·sigdelset——从set中删除指定的信号
信号处理程序的设置方法:
按下 ctrl-z 的结果:输入ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,所示用ps命令可以看到,hello进程并没有被回收。此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。
在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用job查看没有hello进程。
ps\jobs命令截图:
在本章中,阐述进程的定义与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数执行 hello,hello的进程执行,以及hello 的异常与信号处理。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址(Logical Address):指由程序产生的和段相关的偏移地址部分。hello.c经过汇编生成的偏移地址为逻辑地址。例如,C语言中,读取指针变量本身值(&操作),实际上这个值就是逻辑地址。
线性地址(Linear Address):指逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址(段中的偏移地址),加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能经变换产生一个物理地址。若没有启用分页机制,那么线性地址即为物理地址。
虚拟地址(Virtual Address):CPU通过生成虚拟地址访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。Hello可执行目标程序中的地址就是虚拟内存地址。
物理地址(Physical Address):是指目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址即为物理地址。hello在执行过程中通过MMU将程序的虚拟地址转换为物理地址并在地址总线寻找到地址上的内容。
Intel采用段页式存储管理(MMU实现)
段式管理: 逻辑地址->线性地址==虚拟地址
页式管理: 虚拟地址->物理地址
段式管理中,线性地址就是虚拟地址,程序从逻辑地址直接转换为虚拟地址。
段式存储管理中以段为单位分配内存,每段分配一个连续的内存区,但各段之间不要求连续,内存的分配和回收类似于动态分区分配,由于段式存储管理系统中作业的地址空间是二维的,因此地址结构包括两个部分:段号和段内位移。
在段式管理地址变换的过程中间,需要位运行的进程建立一个段表。
段表组成:段号、段长、存储权限、状态、起始地址、修改位、增补位
段的共享和保护机制: 在段式系统中,分段的共享是通过两个作业的段表中相应表目都指向被共享部分的同一个物理副本来实现的。 因为段是一个完整的逻辑信息,所以可以共享,但是页不完整,难以实现共享。不能修改的过程称为纯过程或可重入过程。这样的过程和不能修改的数据是可以共享的,而可修改的程序和数据则不能共享。
从逻辑地址到线性地址的变换过程为:给出一个完整的逻辑地址[段选择符:段内偏移地址]。首先看段选择符判断当前转换时GDT中的段还是LDT中的段,再根据相应寄存器得到其地址和大小。之后拿出段选择符中的前13位,在对应地址中查找到对应的段描述符,这样就知道了基址。根据基址和偏移量结合,就得到了所求的线性地址。
CPU在访问主存前须先将虚拟地址转换成物理地址。MMU(内存管理单元,Memory Management Unit)位于处理器内核和连接高速缓存以及物理存储器的总线之间。当CPU内核取指令或者存取数据的时候,都会提供一个有效地址(effective address,或者称为逻辑地址、虚拟地址)。这个地址是可执行代码在编译的时候由链接器生成的。MMU利用存放在主存中的页表将虚拟地址翻译成物理地址。CPU会通过这个物理地址来访问主存(物理内存)。
线性地址被划分为固定长度的组,成为页。每页包括4KB-4GB字节的地址空间。为了方便叙述,假设48位虚拟地址空间被划分为4KB的页,而每个页表条目都是4字节。
为了节约页表所占用的内存空间,x86-64将线性地址空间通过页目录表和页表多级查找转换成为物理地址。
页表结构:在物理内存中存放着一个叫做页表的数据结构,页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。页表就是一个页表条目(PTE)的数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n位地址字段组成的。有效位表明该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应物理页的起始位置。如果没有设置有效位, 那么一个空地址表示这个虚拟页还未被分配。否则这个地址就指向该虚拟页在磁盘上的起始位置。
CPU中的页表基址寄存器(Page Table Base Register,PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offset,VPO)和一个(n - p)位的虛拟页号(Virtual Page Number,VPN)。MMU利用VPN来选择适当的PTE。例如VPN 0选择PTE 0、VPN1选择PTE1,依此类推。将页表条目中的物理页号(Physical PageNumber,PPN)和虚拟地址中的VPO串联起来,就得到了相应的物理地址。因为物理和虚拟页面都是P字节的,所以物理页面偏移(Physical Page Offset,PPO)和VPO是相同的。
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,会要求从内存多取一次数据,代价是几十到几百个周期。许多系统试图消除这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)。
TLB利用VPN进行寻址,分为组索引(TLBI)和标记(TLBT),用来区别可能映射到同一个TLB组的不同的VPN。
为压缩页表,我们使用层次结构的多级页表。下图以一个两级页表层次结构为例,显示页表的层次结构组成。
以下以Core i7的地址翻译为例,介绍TLB和四级页表支持下的VA到PA的变换。
下图给出Core i7MMU如何使用四级页表将虚拟地址翻译成物理地址。36位VPN被划分为四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供一个到L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供一个到L2 PTE的偏移量,以此类推。
PT:页表、PTE:页表条目、VPN:虚拟页号、VPO:虚拟页偏移、PPN:物理页号、PPO:物理页偏移量
SRAM Cache的概念:cache是为了解决处理器与DRAM(即内存)设备之间巨大的速度差异而出现的。cache属于硬件系统,linux不能管理cache。但会提供flush整个cache的接口。cache分为一级cache,二级cache,三级cache等等。一级cache与cpu处于同一个指令周期。
TLB是MMU中的一块高速缓存,也是一种Cache。
Cache的工作模式:
数据回写(write-back):这是最高性能的模式,也是最典型的,在回写模式下,cache内容更改不需要每次都写回内存,直到一个新的 cache要刷新或软件要求刷新时,才写回内存。
直写(write-through):这种模式比回写模式效率低,因为它每次强制将内容写回内存,以额外地保存cache的结果,在这种模式写耗时,而读和回写模一样快,这都为了内存与cache相一致而付出的代价。
预取 (prefectching):一些cache允许处理器对cacheline进行预取,以响应读请求,样被读取的相邻内容也同时被读出来,如果读是随机的,将会使CPU变慢,预取一般与软件进行配合以达到最高性能。
在访问内存的过程中,先对一级cache进行访问,如果一级cache不命中那么访问二级cache,如果二级cache不命中,那么访问三级cache,依然不命中,那么访问主存,如果主存缺页中断那么就访问硬盘。
shell通过fork为hello创建新进程。当fork 函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
fork结束后,shell的子进程会调用execve装载hello,进而执行hello。execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向hello代码区域的入口点。
缺页故障:内存不命中成为缺页。当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。
缺页中断处理:一个页面就是虚拟内存的一个连续的块。缺页异常调用内核中的缺页异常处理程序,缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:也叫做垃圾收集器,自动释放未使用的已分配的块的过程叫做垃圾收集。例如,诸如Lisp,ML,Java之类的高级语言就依赖垃圾收集来释放已分配的块。
分配的策略有:
1. 记录空闲块:显示空闲链表,隐式空闲链表。分离的空闲链表
2. 防止策略:首次适配,下一次适配,最佳适配
3. 合并策略:立即合并,延迟合并
4. 考虑分割空闲块的实际,对内部碎片忍耐的阈值。
本章主要介绍了操作系统内核的存储管理,包含虚拟内存(段式管理、页式管理),CPU访问主存的方式,动态内存分配等内容。还以Core i7为例介绍了VA到PA的变换。还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理。
设备的模型化:文件
设备管理:unix io接口
输入输出(I/O)是在主存和外部设备之间复制数据的过程。输入操作是从I/O设备复制数据到主存,输出操作是从主存复制数据到I/O设备。
一个linux文件就是一个m个字节的序列:B0,B1,…,Bk,…,Bm-1
所有的I/O设备(例如网络、磁盘和终端)都被模型化文件,而所有的输入输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linuxshell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
UnixI/O函数:
1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:intopen(char *filename, int flags, mode_t mode);
open函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
返回:若成功则为新文件描述符,若出错为-1。
2.进程通过调用close 函数关闭一个打开的文件。
intclose(int fd);
返回:若成功则为0, 若出错则为-1。
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_tread(int fd, void *buf, size_t n);
read函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
ssize_twrite(int fd, const void *buf, size_t n);
write函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。
返回:若成功则为写的字节数,若出错则为-1。
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
进入系统文件可以查看printf的实现代码:
int printf(const char *fmt, ...) {
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt)+4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
首先,arg得到第一个参数的起始地址。接下来调用vsprintf生成显示信息
1. intvsprintf(char *buf, const char *fmt, va_list args)
2. {
3. char* p;
4. chartmp[256];
5. va_list p_next_arg = args;
6.
7. for(p=buf;*fmt;fmt++) {
8. if(*fmt != '%') {
9. *p++= *fmt;
10. continue;
11. }
12.
13. fmt++;
14.
15. switch (*fmt) {
16. case'x':
17. itoa(tmp, *((int*)p_next_arg));
18. strcpy(p, tmp);
19. p_next_arg += 4;
20. p +=strlen(tmp);
21. break;
22. case's':
23. break;
24. default:
25. break;
26. }
27. }
28.
29. return (p - buf);
30. }
vsprintf函数:作用为格式化,接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化输出,其返回值为要打印的字符串长度,即write中的i。
接下来到write系统函数。
1. write:
2. mov eax, _NR_write
3. mov ebx, [esp + 4]
4. mov ecx, [esp + 8]
5. int INT_VECTOR_SYS_CALL
这里传递几个参数给寄存器,以一个int结束,int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。接下来是到陷阱-系统调用 syscall。
1. sys_call:
2. call save
3. push dword [p_proc_ready]
4. sti
5. push ecx //打印出的元素个数
6. push ebx //要打印的buf字符数组中的第一个元素
7. call [sys_call_table + eax * 4] //不断打印字符,直到遇到’\0’
8. add esp, 4 * 3
9.
10. mov [esi + EAXREG - P_STACKBASE], eax
11.
12. cli
13. ret
当字符显示驱动子程序,从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)后,显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
本章介绍了linux下I/O设备的管理方法,将所有设备映射为文件,允许linux内核引出一个简单、低级的应用接口——Unix I/O。以Unix I/O中的实现函数为基础,分析了printf函数和getchar函数的实现方式。
用计算机系统的语言,逐条总结hello所经历的过程。
(1)编写hello程序:通过编辑器将hello的代码输入到计算机中并保存为hello.c文件。
(2)预处理:通过命令cpp hello.c > hello.i将 hello.c 调用的所有外部的库展开合并到一个 hello.i 文件中
(3)编译:通过命令gcc -Shello.i -o hello.s将 hello.i 编译成为汇编文件 hello.s
(4)汇编:通过命令as hello.s -o hello.o将 hello.s 会变成为可重定位目标文件 hello.o
(5)链接:通过一系列的命令将 hello.o 与可重定位目标文件和动态链接库链接成为可执行目标程序 hello
(6)运行:在 shell 中输入./hello 120L022115 王炳轩
(7)创建子进程:shell 进程调用 fork 为其创建子进程
(8)加载程序:shell 调用 execve,execve 调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main 函数。
(9)程序的运行过程:如果运行途中键入 ctr-c ctr-z 则调用 shell 的信号处理函数分别停止、挂起。
(10)资源回收:shell 父进程回收子进程,内核删除为这个进程创建的所有数据结构。
列出所有的中间产物的文件名,并予以说明起作用。
文件名 |
作用 |
hello.c |
程序源代码 |
hello.i |
hello.c经预处理之后得到的程序文件 |
hello.s |
hello.i文件经编译后得到的汇编语言源代码文件 |
hello.o |
hello.s文件经汇编后得到的可重定位目标文件 |
hello |
hello.o文件经过链接后得到的可执行目标文件 |
hello.o.objdump.txt |
hello.o的反汇编代码 |
hello.objdump.txt |
hello的反汇编代码 |
[1] printf函数的实现剖析:https://www.cnblogs.com/pianist/p/3315801.html
[2] x86的四种地址:https://www.cnblogs.com/bhlsheji/p/4868964.html
;https://blog.csdn.net/rabbit_in_android/article/details/49976101
[3] gcc编译的四个步骤:https://blog.csdn.net/xiaohouye/article/details/52084770
[4] gcc -s的分析:http://blog.chinaunix.net/uid-20508657-id-1939444.html
[5] ELF格式分析:https://blog.csdn.net/edonlii/article/details/8779075
[6] 兰德尔E.布莱恩特 大卫R.奥哈拉伦.深入理解计算机系统(第三版).机械工业出版社.2018.4.
[7] C语言预处理命令详解:
https://blog.csdn.net/qq_40757240/article/details/88763441
[8] Pianistx:printf函数的深入剖析:https://www.cnblogs.com/pianist/p/3315801.html
[9] 段式管理:
段式管理_weixin_30421525的博客-CSDN博客
[10] 李忠,王晓波,余洁著. x86汇编语言 从实模式到保护模式. 北京:电子工业出版社, 2013.01.
[11] 吴锐. 计算机系统课程PPT, 哈尔滨工业大学, 2022.