摘 要
作为险些全世界每个程序员的初恋,HelloWorld用超越时间的本领见证着每一位在将来驰骋于二进制世界的程序员在初次编程时的羞涩与鸠拙,在他们鸠拙地点击集成开发环境中的运行按钮后,至此潘多拉的魔盒已被开启……在他们不断的解决一起走来的各种bug和底层问题,慢慢的一步一步成长为更加强大经验丰富的程序员,他们终有一天会回到这个起点,意识到当年那个稀松平常的字符串HelloWorld出现在控制台背后所蕴藏着的机密且巨大的聪明英华;
本文围绕简单的C语言程序hello辉煌光耀辉煌的一生,对hello程序“From Program to Process”和“From Zero-0 to Zero-0”两个过程展开分析,同时对hello.c从预处理、编译、汇编、链接终极成为了可以实行的文件hello,然后在CS中完成被folk成为历程,被execve加载,颠末对控制流的管理,内存空间的分配,异常的处理,对I/O设备的调用,终极被shell回收的完备一生进行回顾,并借此深入明确计算机系统各个部门运作的方式与机制,较为完备地显现x86-64计算机系统地主要工作机制以及顶层程序员与底层呆板的沟通逻辑与原理。
关键词:计算机系统;利用系统;程序;预处理;编译;汇编;链接;历程管理;存储管理;I/O管理
目 录
第1章 概述... - 5 -
1.1 Hello简介... - 5 -
1.1.1. P2P:From Program to Process. - 5 -
1.1.2. 020:From Zero-0 to Zero-0. - 5 -
1.2 环境与工具... - 5 -
1.3 中间效果... - 6 -
1.4 本章小结... - 6 -
第2章 预处理... - 7 -
2.1 预处理的概念与作用... - 7 -
2.1.1.预处理的概念... - 7 -
2.1.2.预处理的作用... - 7 -
2.2在Ubuntu下预处理的命令... - 7 -
2.3 Hello的预处理效果解析... - 7 -
2.4 本章小结... - 8 -
第3章 编译... - 9 -
3.1 编译的概念与作用... - 9 -
3.1.1.编译的概念... - 9 -
3.1.2.编译的作用... - 9 -
3.2 在Ubuntu下编译的命令... - 9 -
3.3 Hello的编译效果解析... - 10 -
3.3.1.变量... - 10 -
3.3.2.常量数据和赋值... - 11 -
3.3.3.算数利用... - 11 -
3.3.4.关系利用及控制转移... - 11 -
3.3.5. 函数利用... - 12 -
3.4 本章小结... - 12 -
第4章 汇编... - 13 -
4.1 汇编的概念与作用... - 13 -
4.1.1.汇编的概念... - 13 -
4.1.2.汇编的作用... - 13 -
4.2 在Ubuntu下汇编的命令... - 13 -
4.3 可重定位目标elf格式... - 13 -
4.3.1.格式图片与readelf命令... - 13 -
4.3.2. hello.o的elf头... - 14 -
4.3.3. hello.o的节头表... - 14 -
4.3.4. hello.o的重定位节... - 15 -
4.3.5. hello.o的符号表... - 15 -
4.4 Hello.o的效果解析... - 15 -
4.5 本章小结... - 16 -
第5章 链接... - 17 -
5.1 链接的概念与作用... - 17 -
5.1.1.链接的概念... - 17 -
5.1.2.链接的作用... - 17 -
5.2 在Ubuntu下链接的命令... - 17 -
5.3 可实行目标文件hello的格式... - 17 -
5.3.1. hello的ELF头... - 18 -
5.3.2. hello的节头表... - 18 -
5.3.3. hello的程序头表... - 19 -
5.4 hello的虚拟地址空间... - 20 -
5.5 链接的重定位过程分析... - 22 -
5.5.1.新增函数... - 22 -
5.5.2.新增节... - 22 -
5.5.3.函数调用地址... - 23 -
5.5.4.控制流跳转地址... - 23 -
5.6 hello的实行流程... - 23 -
5.7 Hello的动态链接分析... - 24 -
5.8 本章小结... - 25 -
第6章 hello历程管理... - 26 -
6.1 历程的概念与作用... - 26 -
6.1.1.历程的概念... - 26 -
6.1.2.历程的作用... - 26 -
6.2 简述壳Shell-bash的作用与处理流程... - 26 -
6.3 Hello的fork历程创建过程... - 26 -
6.4 Hello的execve过程... - 27 -
6.5 Hello的历程实行... - 27 -
6.5.1.逻辑控制流... - 27 -
6.5.2.历程时间片... - 27 -
6.5.3.用户与内核模式... - 28 -
6.5.4.历程的上下文切换... - 28 -
6.6 hello的异常与信号处理... - 28 -
第7章 hello的存储管理... - 30 -
7.1 hello的存储器地址空间... - 30 -
7.1.1.逻辑地址... - 30 -
7.1.2. 线性地址... - 30 -
7.1.3.虚拟地址... - 30 -
7.1.4. 物理地址... - 30 -
7.2 Intel逻辑地址到线性地址的变更-段式管理... - 30 -
7.3 Hello的线性地址到物理地址的变更-页式管理... - 31 -
7.4 TLB与四级页表支持下的VA到PA的变更... - 31 -
7.5 三级Cache支持下的物理内存访问... - 32 -
7.6 hello历程fork时的内存映射... - 32 -
7.7 hello历程execve时的内存映射... - 33 -
7.8 缺页故障与缺页中断处理... - 33 -
7.9动态存储分配管理... - 34 -
7.10本章小结... - 34 -
第8章 hello的IO管理... - 35 -
8.1 Linux的IO设备管理方法... - 35 -
8.2 简述Unix IO接口及其函数... - 35 -
8.2.1.Unix I/O接口... - 35 -
8.2.2.Unix I/O函数... - 35 -
8.3 printf的实现分析... - 36 -
8.4 getchar的实现分析... - 37 -
8.5本章小结... - 38 -
结论... - 39 -
附件... - 40 -
参考文献... - 41 -
第1章 概述
1.1 Hello简介
1.1.1.P2P:From Program to Process
即从源文件到可实行目标文件的转化:对于用高级语言编写的hello.c文件在Unix系统上是由GCC编译器先读取源文件hello.c,并把它编译为一个可以实行的目标文件hello。这个编译过程可以分为四个阶段:首先预处理器(cpp)将源程序hello.c修改成hello.i文本文件,接着编译器(ccl)将hello.i翻译成汇编程序hello.s,汇编器(as)将hello.s翻译成呆板语言指令,并将这些指令打包成可重定位目标程序hello.o,链接器(ld)将hello.o与库函数相链接终极生成可实行目标程序hello。而后要实行该程序时,利用系统(CS)借助folk创建一个子历程,至此hello.c从从一个program(程序)变成了一个process(历程)
1.1.2.020:From Zero-0 to Zero-0
即:历程从无到有的产生以及,终极被回收消散再化为无的过程:
shell使用execve函数加载并运行可实行目标文件hello,利用系统为hello分配虚拟内存地址VA,在物理内存地址PA与虚拟内存地址VA之间建立映射。而且在实行的过程中虚拟内存为历程提供独立的内存空间,TLB、4级页表、3级Cache以及Pagefile文件保证了数据从磁盘到CPU的高效传输,I/O管理与信号处理共同实现了hello的输入输出。在程序运行结束后,shell使用父历程负责回收hello历程,对应的虚拟内存空间被释放。至此hello历程便完成了从无(0)到有再到无(0)的全过程。
1.2 环境与工具
1.2.1.硬件环境
处理器:Intel core i7-12700H CPU; RAM:32.00GB
1.2.2.软件环境
Windows 11 64位,VMware Workstation Pro,Ubuntu 22.04.03 LTS 64位
1.2.3.开发与调试工具
gcc/ as /ld /Vim /Edb /readelf /gedit /Visual Studio /CodeBlocks
1.3 中间效果
- hello.c:源程序文件
- hello.i:hello.c预处理后的源程序文件
- hello.s:hello.i编译后的汇编程序
- hello.o:hello.s汇编后的可重定位目标文件
- elf.txt:hello.o的ELF文件
- dump_hello.txt:hello.o的反汇编代码
- hello:hello.o链接后的可实行目标文件(不包罗可调试选项)
- hello1:hello.o链接后的可实行目标文件(包罗可调试选项)
- hello_objdump.s:hello的反汇编代码
1.4 本章小结
本章通过简述hello的完成生命过程,简单阐释了从编写、预处理、编译、汇编、链接形成历程以及该历程从无到有再到历程结束后被回收的各个阶段。
同时本章也枚举出了实验所使用的硬件环境、软件环境以及编译调试工具,而且枚举出了生成的各个中间文件的名称,以及其用途和对研究过程产生的作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1.预处理的概念
预处理一般指的就是预处理器(cpp)根据源代码中的预处理指令等信息对源代码文件(文本文件)进行修改更换成为新的程序文件(通常是以.i作为文件扩展名)的过程。
比方读取头文件<stdio.h>的内容,而且把它直接插入到程序文本中,对#define界说的宏进行更换,根据条件选择#if内的代码等。
2.1.2.预处理的作用
预处理实现了在编译器进行编译之前对源代码做针对预处理指令的转换。比方1.宏更换(将宏界说的名称更换为字符串或数值)、2.文件包罗(读取头文件中的内容并将其直接插入到程序文本中)、3.条件编译(根据条件编译指令决定需要编译的代码)、4.删除注释等功能。
其中预处理指令一般被用来使源代码在差别的实行环境中被方便的修改或者编译。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -m64 -no-pie -fno-stack-protector -fno-PIC -E hello.c -o hello.i
图1-在Ubuntu下的预处理过程
2.3 Hello的预处理效果解析
打开hello.i文件,发现由原来的24行变成了3092行。其中先前的三个头文件中的内容被直接插入到文本文件中,同时本来文件中的注释也被删除,但main函数没有改变。且在最开头包罗了hello.c涉及的全部头文件的信息,末了是真正的hello.c的内容:
图2-hello.i部门代码截图
2.4 本章小结
本章通过详细阐述hello.c颠末预处理器(cpp)变成hello.i文件的过程,简要表明了预处理的概念,对预处理过后的文件进行了简要的分析:预处理器实质上就是对源代码进行了大量的展开,扩充了本来include的头文件,以及更换了一些预处理指令宏界说。
从这里也可以看出hello程序终极将字符呈现在显示器上的过程背后隐藏了许许多多我们并未注意到的东西,其机密还远不止于此。
第3章 编译
3.1 编译的概念与作用
3.1.1.编译的概念
当hello.c源程序颠末预处理器(cpp)处理后,编译器(ccl)会通过词法分析和语法分析等过程,将原始的补全的C语言语法格式的代码转换为面向CPU的呆板指令,转换后的效果以汇编语言的文本形式保存。
将先前得到的文本文件 hello.i 翻译成文本文件 hello.s。
其中编译器(ccl)将高级语言转换为低级语言的翻译过程可以分为以下5个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。其中最主要的是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,则会给出提示信息。
3.1.2.编译的作用
编译的作用主要与前述的翻译过程相关,最主要的则是能够将人们所认识的,更符合人的逻辑习惯的高级语言转换成离计算机更近的低级语言,其各个过程的作用如下:
1.语法分析词法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
2.中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比力容易实现中间代码。
3.代码优化:指对程序进行多种等价变更,使得从变更后的程序出发,能生成更有效的目标代码。
4.目标代码:生成是编译的末了一个阶段。目标代码生成器把语法分析后或优化后的中间代码变更成目标代码。此处指汇编语言代码,须颠末汇编程序汇编后,成为可实行的呆板语言代码
3.2 在Ubuntu下编译的命令
编译命令:gcc -m64 -no-pie -fno-stack-protector -fno-PIC -S hello.i -o hello.s
图-3在Ubuntu下的编译过程
3.3 Hello的编译效果解析
3.3.1.变量
全局变量:
已经初始化而且初始值非零的全局变量储存在.data节,未初始化的全局变量存储在.bss节在加载进入内存时释放空间,它的初始化不需要汇编语句,而是通过虚拟内存哀求二进制零的页直接完成的。
局部变量:
hello的源代码中并没有全局变量,局部变量分别是函数参数argc和argv,以及for循环中的i;其中argc表示参数argv的个数,argc、数组argv以及i均存储在栈中,且argc地址为-20(%rbp); argv首地址为-32 (%rbp),每个参数加8(在linux64位系统中每个指针占8字节),即argv[k]=-(32+8k)(%rbp);i地址为-4(%rbp)。
hello.c源码与汇编代码的对比:
图-4 hello.i(左)与hello.s局部变量代码对照
3.3.2.常量数据和赋值
常量数据:
在hello.c的main函数中,常量包括printf函数中打印的两个字符串常量和if条件、for循环里的数字常量。其中如图6,所示字符串常量被存储在./rotate段,如图7所示,数字常量被存储在.text段中,且作为立即数出现:
图-5在hello.s中的字符串常量
图-6 hello.i(左)与hello.s(右)数字常量对照
赋值:
如图4左上及右边白色方框所示,右边的hello.c源码中,对与局部变量i进行赋值初始化,即:将0赋给i,在汇编代码中对应第一行“movl $0, -4(%rbp)故在汇编指令中使用movl x y 的形式进行直接赋值利用。
3.3.3.算数利用
在hello.c源代码中对于循环利用,使用了++利用符;对应的汇编代码为,对i自加,栈上存储变量i的值加1:
图-7 hello.i(左)与hello.s(右)算数操尴尬刁难照
3.3.4.关系利用及控制转移
if条件判定时所使用的“!=”和“<”是逻辑关系利用,if-else或者是否跳出for循环则是控制转移利用,在汇编代码中,关系利用往往与控制转移(跳转指令)一起出现,即“cmp”后会跟上“jXX”,是否进行控制转移取决于cmp设置的逻辑关系利用的效果位,亦或者直接实行跳转指令“jmp”;
如图8中,最上方展示了在对i进行初始化赋值后直接实行跳转指令“jmp”以及对于argc与4的比力与控制转移:首先将立即数4与-20(%rbp)对应值(即argc)进行对比,若即是,即“je”就代表相称则跳转至.L2。在for循环中源代码设置了i<8的比力作为循环进入条件,在汇编指令中实行-4(%rbp)与立即数7的比力,小于即是则跳转至.L4:
图-8 hello.i(左)与hello.s(右)控制转移对照
3.3.5. 函数利用
在hello.i中一共涉及了5个函数调用,即main主函数、printf、exit、sleep、getchar。对于linux X86-64系统中,函数调用通报参数的规则:第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下多余的参数保存在栈当中。其次,若有多个参数,则将参数存储在栈中;调用函数前都有mov指令,按照参数逐个从后向前的顺序,其中比力典范的就是调用atoi和sleep函数前都将寄存器%rax中的值传送到储存第一个参数的寄存器%rdi中,而像getchar这样没有参数的函数,则无需实行以上利用。对于main函数返回即return 0,汇编代码中则使用“movl $0, %eax”,“leave”,“ret”,即将0传送到%eax,然后使用leave规复调用者即:caller的栈帧,清理被调用者即:callee的栈帧,末了使用ret指令返回即找到下一条指令的地址。
图-9 hello.s中的函数调用与参数通报
3.4 本章小结
本章详细先容了编译的概念以及过程。使用Ubuntu下的编译指令完成了对hello.i的编译工作,将hello.i的文本文件转换为了hello.s的汇编语言文件,
此外,本章通过与源文件C程序代码以及所生成的汇编语言指令的比力,对汇编代码进行了解析,先容了汇编代码怎样实现全局与局部变量,常量数据与赋值,关系利用与控制转移以及函数调用等过程。完成该阶段转换后,hello的生命历程进入了下一阶段,即:汇编处理。
第4章 汇编
4.1 汇编的概念与作用
4.1.1.汇编的概念
当hello.i源程序颠末编译器(ccl)处理后,汇编器(as)会将汇编语言的ascII码文件(此时是是hello.s)翻译成呆板语言指令,而且打包成可重定位目标文件格式(hello.o),此过程被称为汇编。
4.1.2.汇编的作用
可重定位目标文件(hello.o)是一个二进制文件,它包罗了程序的二进制指令编码。汇编器(as)将.s 汇编程序翻译成呆板语言指令,并将这些二进制指令编码以可重定位目标文件的格式保存在.o目标文件中,用于后续的链接。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -m64 -no-pie -fno-stack-protector -fno-PIC -c hello.s -o hello.o
也可以使用as汇编器直接汇编:as hello.s -o hello.o
图-10 在Ubuntu下的汇编过程
4.3 可重定位目标elf格式
4.3.1.格式图片与readelf命令
使用命令:readelf -a hello.o > ./elf.txt 来查察hello.o的elf文件;
可重定位目标elf格式中包罗各个节以及其所含信息如下:
图-11 可重定位目标文件elf格式各节内容
4.3.2. hello.o的elf头
首先是elf头,如图12所示包括16字节标识信息、文件类型、呆板类型、节头表偏移、节头表的表项大小以及表项个数。
图-12 可重定位格式目标文件elf头
4.3.3. hello.o的节头表
hello.o的节头表 描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
图-13 可重定位格式目标文件节头表
4.3.4. hello.o的重定位节
各个段引用的外部符号等在链接时需要通过重定位对这些位置的地址进行修改。链接器会通过重定位节的重定位条目计算出正确的地址。
hello.o需重定位的内容:.rodata中的模式串,puts,exit,printf,atoi,sleep,getchar等符号。
图-14 可重定位格式目标文件重定位节
4.3.5. hello.o的符号表
.symtab存放在程序中界说和引用的函数和全局变量的信息。如图14所示,包括Value、Size、Type、Bind、Vis、Ndx、Name等信息。
图-15 可重定位格式目标文件符号表
4.4 Hello.o的效果解析
反汇编命令:objdump -d -r hello.o > dump_hello.txt
首先汇编语言与呆板语言构造形式差别,hello.s开头包罗常量变量的描述,而hello.o的反汇编代码则没有变量和常量的描述;而且呆板语言的构成与汇编语言属于一一映射关系;如图15框出内容所示,hello.o的反汇编代码在每条指令前还有相对应的呆板码的十六进制表示。
图-16 hello.o反汇编代码与hello.s对比
其次如图15圆圈所示,hello.s中的利用数用十进制表示,而 hello.o的反汇编代码中的利用数用十六进制表示。
如图16所示,hello.s在跳转时,控制转移指令(je)后接的是段名(.L2),而在hello.o的反汇编代码中则使用了十六进制相对地址;如图16所示,hello.s在函数调用时,函数调用指令(call)跟的是函数名(puts),而在hello.o的反汇编代码中则同样使用了十六进制相对地址。
图-17 hello.o反汇编代码与hello.s控制转移与函数调用对比
4.5 本章小结
本章对于“编译”过程进行了展开描述,首先先容了汇编的概念和作用,之后在Linux中将hello.s汇编成hello.o,并分析了hello.o的elf文件以及对比hello.s与hello.o的反汇编代码的接洽与区别。
至此最初的hello.c源代码文本文件已经变成了现在的hello.o二进制文件,此时二进制文件已经可以被CPU所识别,但是现在的hello.o仍没有与内存、库函数的代码、数据相融合,因此仍然无法作为终极的可实行目标文件。在颠末接下来的章节:链接之后,hello将终极成形,变成终极的可实行目标文件。
第5章 链接
5.1 链接的概念与作用
5.1.1.链接的概念
所谓“链接”指的是:使用连接器(ld)将各种差别文件(主要是可重定位目标文件)的代码和数据综合在一起,通过符号解析和重定位等过程,终极组合成一个可以在程序中加载和运行的单一的可实行目标文件的过程。
5.1.2.链接的作用
链接使得分离编译成为可能,即:不需要将一个大型的应用程序构造成一个巨大的源文件,而是可以将其分解成更小的、更好管理的模块,可以独立的修改和编译这些文件模块。
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
图-18 在Ubuntu下使用ld的链接过程
5.3 可实行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
可实行目标文件的格式与可重定位目标文件的格式雷同:ELF头描述文件的总体格式,与此同时还包括了程序的入口点,即:程序运行时要实行的第一条指令的地址。.text,.rodata以及.data节与可重定位目标文件的节是相似的;其中差别的是可实行目标文件elf格式多了.init节,并界说了一个小函数,叫做_init,程序的初始化代码会调用它。因为可实行文件是完全链接的(已被重定位),所以也不再有rel节,即:重定位节。
本文使用readelf指令查察hello的ELF格式,包括各段起始地址,大小等信息。
图-19 可实行文件的ELF结构图
5.3.1. hello的ELF头
使用readelf -h hello命令查察hello的ELF头:
图-20 可实行文件hello的ELF头
5.3.2. hello的节头表
使用readelf -S hello命令查察hello的节头表:
图-21 可实行文件hello的节头表
5.3.3. hello的程序头表
调用readelf -l hello查察hello的程序头表:
图-22 可实行文件hello的程序头表1
图-23 可实行文件hello的程序头表2
其中Offset代表目标文件中的偏移,VirtAdrr代表虚拟内存的地址,PhysAddr代表物理地址的内存,FileSiz代表目标文件中的段大小,MemSiz代表内存中的段大小,flags代表运行时的访问权限,align代表对齐要求。
5.4 hello的虚拟地址空间
使用edb加载hello, data dump窗口可以查察加载到虚拟地址中的hello程序。program headers告诉链接器运行时加载的内容并提供动态链接需要的信息。
由下图可以得到,虚拟地址空间的起始位置是0x400000:
图-24 hello的虚拟地址空间起始位置
由前文中图21可知,.interp段的起始地址为0x400000+0x0002e0=0x4002e0,在edb中查找地址得到如下图的效果:
图-25 hello的.interp节存储内容
由前文中图21可以得到,.rodata的起始地址为0x402000,在edb中查询地址可以得到如下图的效果:
图-26 hello的.rodata节存储内容
5.5 链接的重定位过程分析
反汇编命令:objdump -d -r hello > hello_objdump.s
图-27 hello的反汇编代码中.init函数
5.5.1.新增函数
链接加入了在hello.c中用到的库函数,如printf、getchar、exit、sleep等函数。
图-28 hello的反汇编代码中的新增函数
5.5.2.新增节
hello中增长了如图27与图2所示的.init和.plt节:
图-29 hello的反汇编代码中的新.plt节
5.5.3.函数调用地址
Hello已经完成了调用函数时的重定位,因此在调用函数时调用的地址已经变成了函数确切的虚拟地址:
图-30 hello的反汇编代码中的函数调用使用的地址
5.5.4.控制流跳转地址
Hello已经完成了调用函数时的重定位,因此在跳转时调用的地址已经变成了函数确切的虚拟地址。
图-31 hello的反汇编代码与hello.o反汇编代码的控制流跳转对比
5.6 hello的实行流程
现在对hello.o进行重新编译,加入-g便于后续使用edb调试:
命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello1
图-32 hello1的函数列表
5.7 Hello的动态链接分析
程序调用一个有共享库界说的函数时,由于该函数地点的共享模块可能可以被加载到内存的任何位置。因此,编译系统采用延迟绑定,将过程地址的绑定推迟到第一次调用该过程的时候。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。其中PLT是一个数组,其中每个条目是16字节代码。PLT [0]是一个特殊条目,它跳转到动态链接器中。GOT是一个数组,其中每个条目是8字节地址。GOT [2]是动态链接器在1d-linux.so模块中的入口点。每个条目都有一个相匹配的PLT条目。
在5.3部门图21可以看到hello的ELF文件,有GOT运行时的地址0x403ff0和PLT运行时的地址0x404000。因此分别在dl_init调用前后查察地址,如图33与图34所示,可以发现在dl_init调用前后,GOT运行时的地址对比:
图-33 调用dl_init前PLT的内容
图-34 调用dl_init后PLT的内容
5.8 本章小结
本章围绕链接的过程进行了详细展开。首先先容了链接的概念和作用,而后将hello.o链接得到hello,并通过edb查察hello的虚拟地址空间,以及hello与hello.o反汇编代码的对比,详细阐述了链接过程中的重定位过程、实行流程、动态连接过程。
至此,hello程序已经预备停当,hello的人生旅途已经走过了一半的时间,接下来hello将真正进入CPU,通过shell的构造成为一个在利用系统中运行的历程!
第6章 hello历程管理
6.1 历程的概念与作用
6.1.1.历程的概念
历程(Process)指的是一个实行中的程序的实例;是计算机中的程序关于某数据集合上的一次运行活动,同时也是系统进行资源分配和调度的基本单位,是利用系统结构的基础。
系统中的每个程序都运行在某个历程的上下文中。程序的上下文由程序正确运行所需要的状态构成:包括存放在内存中的程序的代码和数据、栈、通用寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.1.2.历程的作用
历程给程序提供了一个独立的逻辑控制流,提程序独占使用处理器的假象,提供了程序独占使用内存系统的假象;历程给程序提供了一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个用户级的交互型应用程序,代表用户实行利用系统中的任务。处理hello的流程如下:
- 首先对于输入的命令行“$./hello”进行解析:使用表明器构造argv和envp求值,发现该命令行第一参数并不是shell-bash的内置命令
- 于是shell调用fork函数创建子历程,其地址空间与shell父历程完全相同,包括只读代码段、读写数据段、堆及用户栈等
- 展开一个新的历程后,shell调用execve函数在当进步程(新创建的子历程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当进步程的虚拟地址空间;同时如果execve函数在当前路径内未找到hello这个可实行程序,则会显示一条错误信息;
- 末了hello中的main函数被调用实行
6.3 Hello的fork历程创建过程
父历程通过调用fork函数创建一个新的运行的子历程。子历程中,fork返回0;父历程中,返回子历程的PID;此时子历程与父历程具有以下性质:
- 子历程得到与父历程虚拟地址空间相同的(但是独立的)一份副本;
- 子历程还获得与父历程任何打开文件描述符相同的副本,即:子历程能够读写父历程所打开的任何文件;
- 子历程获得与父历程任何打开文件描述符相同的副本;
- 最大区别:子历程有差别于父历程的PID。
- 父历程和子历程是并发运行的独立历程。内核以恣意方式交替实行其逻辑控制流的指令,因此无法预测实际父历程与子历程的运行先后;
6.4 Hello的execve过程
execve函数原型:int execve(const char *filename, char *const argv[ ], char *const envp[ ])
execve函数加载并运行可实行目标文件hello,会首先在目标路径中寻找hello文件,而且为hello创建一个内存映像,
映射私有区:为该程序的栈区域创建新的区域结构将可实行文件的片复制到代码段和数据段等;
然后映射共享区:为共享库建立映射空间,比方Hello程序与标准C库libc.so链接;
末了设置PC:设置当进步程上下文的程序计数器,将其指向入口函数,并将控制通报给新程序的主函数。
只有当出现错误时,比方找不到hello,execve函数才会返回-1到调用程序。否则execve函数一次调用且从不返回。
6.5 Hello的历程实行
6.5.1.逻辑控制流
历程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并实行,其中这些PC值的序列叫做逻辑控制流,而且这些值与可实行目标文件的指令或者包罗在运行时动态链接到程序的共享对象中的指令一一对应。
6.5.2.历程时间片
利用系统会对历程的运行进行调度,实行历程A->上下文切换->实行历程B->上下文切换->实行历程A->… 云云循环往复。在历程实行的某些时刻,内核可以决定抢占当进步程,并重新开始一个先前被抢占了的历程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的历程运行,我们说内核调度了这个历程。在内核调度了一个新的历程运行了之后,它就抢占了当进步程,并使用上下文切换机制来将控制转移到新的历程。在一个程序被调运行开始到被另一个历程打断,中间的时间就是运行的时间片。
其中一个逻辑流的实行在时间上与另一个流重叠被称为并发流,这两个流并发运行。多个流并发实行的概念被称为并发。一个历程与其他历程轮流运行的概念称为多任务。一个历程实行其控制流一部门的每一个时间段叫做时间片,多任务也就被称作是时间分片。
6.5.3.用户与内核模式
用户模式的历程不答应实行特殊指令,不答应直接引用地址空间中内核区的代码和数据。
内核模式历程可以实行指令集中的任何命令,而且可以访问系统中的任何内存位置。
运行程序代码初始时都是在用户模式中的,当发生中断故障或系统调用的异常时,历程从用户模式转变为内核模式。当异常发生时,控制通报到异常处理程序,处理器将模式转变为内核模式。
6.5.4.历程的上下文切换
在内核调度了一个新的历程运行后,它就抢占当进步程,并使用一种称为上下文切换的机制来将控制转移到新的历程,上下文切换——①保存当进步程的上下文,②规复某个先前被抢占的历程被保存的上下文,③将控制通报给这个新规复的历程。
图-35 历程上下文切换示意图
6.6 hello的异常与信号处理
在shell运行hello时并键盘输入:
ctrl+C(发送一个SIGINT信号给hello历程,使hello停止)
ctrl+Z(发送一个SIGTSTP信号给前台程序,即hello,使其被挂起)
ps(列出当前全部历程)
jobs(显示当前暂停的历程)
pstree(查察历程树)
fg(使hello任务在前台继承进行)
kill(发送一个SIGINT信号给hello历程,使hello停止)
一系列指令并查察对应效果,效果如图36-38所示:
图-36 正常运行后键盘输入Ctrl+C
图-37 正常运行后键盘输入ctrl+Z、ps、jobs、pstree
图-38 键盘输入ctrl+Z后输入fg、kill
6.7本章小结
本章详细展开了hello历程的实行过程:首先对于在hello运行过程中,历程、shell-bash、fork、execve、历程实行和异常与信号处理的机制这些历程相关利用进行详细阐明;之后从逻辑控制流、时间分片、用户模式/内核模式、上下文切换等角度详细分析了历程的实行过程,末了分析了hello的异常与信号处理机制,并演示了差别情况下hello出现异常的详细情况。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1.逻辑地址
逻辑地址指的是:程序颠末编译后出现在汇编代码中的地址,是由程序产生的与段相关的偏移地址部门,也叫相对地址;表示成段标识符:段内偏移量;
hello.o中所使用的即为逻辑地址,要颠末寻址方式的计算或变更才得到内存储器中的实际有效地址,即物理地址。
7.1.2. 线性地址
线性地址是逻辑地址到物理地址变更之间的中间层。代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
7.1.3.虚拟地址
CPU启动保护模式之后,程序运行在虚拟地址空间中,虚拟地址空间是全部可能地址的集合,对于一个64位的呆板而言,则集合中共有2^64种可能。
但并非全部程序均运行在虚拟地址当中,CPU在启动的时候是运行在实模式的,是直接使用物理地址的。
7.1.4. 物理地址
物理地址指的是:在存储器中以字节为单位存储信息,与每一个字节单元一一对应的一个唯一的存储器地址;又叫实际地址或者是绝对地址,放在寻址总线上的地址。。物理地址对应了系统中实际的内存字节。
7.2 Intel逻辑地址到线性地址的变更-段式管理
在 Intel 平台下,逻辑地址是 selector ffset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。这个过程就称作段式内存管理。
逻辑地址由段标识符和段内偏移量构成。段标识符是一个16位长的字段(段选择符),可以通过段标识符的前13位,直接在段描述符表中找到一个详细的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
给定一个完备的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0照旧1,知道当前要转换是GDT中的段,照旧LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。Base + offset = 线性地址。
7.3 Hello的线性地址到物理地址的变更-页式管理
所谓页式管理,即:分页机制是指将虚拟内存分割为虚拟页的大小固定的块。将各历程的虚拟空间分别成多少个长度相称的页,把内存空间按页的大小分别成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变更机构。页式管理采用哀求调页或预调页技术实现了内外存存储器的统一管理。
为判定虚拟页是否缓存在DRAM的某个地方,并确定虚拟页地点的物理页位置,因此需要一个页表。页表是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个n位地址字段构成,有效位的设置与否表明了虚拟页是否被DRAM缓存。在地址翻译过程中,需要用到一个页表基址寄存器,指向当前页表。一个n位虚拟地址包罗两个部门,一是p位的虚拟页面偏移,二是一个n-p位的虚拟页号。在虚拟页已被缓存的情况下,页面掷中,MMU利用VPN来选择合适的PTE,将页表条目中的物理页号和虚拟地址中的虚拟页面偏移联合起来就可以得到一个物理地址。而如果虚拟页没有被缓存,那么就需要处理缺页,触发缺页异常,并将控制通报到缺页异常处理程序,缺页处理程序能够确定物理内存中的牺牲页,进而判定是否因被修改过而需要调出内存,完成牺牲页的更换之后,则调入新的页面,并更新内存中的PTE,末了返回到原来源程,将引起缺页的虚拟地址重新发给MMU,这次便变成了掷中的情况
7.4 TLB与四级页表支持下的VA到PA的变更
TLB是指:一个位于MMU中,关于PTE的一个缓存,被称为快表。快表是一个小的、虚拟寻址的缓存,其中每一行均保存了一个由单个PTE构成的块。TLB有高度的相联性。
四级页表是一种多级页表,多级页表的主要目的是用于压缩页表。在地址翻译过程中,虚拟的地址页号VPN被分为了k个,每一个VPNi都是一个指向第i级页表的索引。当1 <= j <= k-1时,都是指向第j+1级的某个页表。第k级页表中的每个PTE包罗某个物理页面的PPN,或者时一个磁盘块的地址。
图-39 PTE构成结构
TLB通过虚拟地址VPN部门进行索引,分为索引(TLBI)与标记(TLBT)两个部门。这样,MMU在读取PTE时会直接通过TLB,如果不掷中再从内存中将PTE复制到TLB。在以上机制的基础上,如果所使用的仅仅是虚拟地址空间中很小的一部门,那么仍然需要一个与使用较多空间相同的页表,造成了内存的浪费。所以虚拟地址到物理地址的转换过程中还存在多级页表的机制:上一级的页表映射到下一级也表,直到页表映射到虚拟内存,如果下一级内容都未分配,那么页表项则为空,不映射到下一级,也不存在下一级页表,当分配时再创建相应页表,从而节约内存空间。
图-40 VA到PA的变更
7.5 三级Cache支持下的物理内存访问
当MMU按照7.4所述的利用获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CI(组索引),CO(块偏移)。根据CI寻找到正确的组,依次与每一行的数据比力,有效位有效且标记位一致则掷中。如果掷中,直接返回想要的数据。如果不掷中,就依次去L2,L3,主存判定是否掷中,掷中时将数据传给CPU同时更新各级cache的储存。
7.6 hello历程fork时的内存映射
shell通过fork为需要运行的非内置命令的程序创建新历程,当fork函数被当进步程调用时,内核为新历程创建各种数据结构,并分配给它历程唯一的PID。为了给这个新历程创建虚拟内存,它创建了当进步程的mm_struct、区域结构和样表的原样副本。它将两个历程中的每个页面都标记为只读,并将每个历程中的每个区域结构都标记为写时复制。
当fork在新历程中返回时,新历程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个历程中的任一个后来进行写利用时,写时复制机制就会创建新页面。
7.7 hello历程execve时的内存映射
Execve函数加载并运行可实行目标文件程序需要以下几个步骤:
- 删除已存在的用户区域:删除历程虚拟地址中已存在的用户区域;
- 映射私有区域:为新程序的代码、数据、bsss和栈区域创建新的区域结构。这些区域都是私有的、写时复制的。代码和数据区域被映射成a.out文件中的.text和.data区。bss区域是哀求二进制零的,映射到匿名文件,其大小包罗在a.out中。栈和堆区域也是哀求二进制零的,初始长度为零。
- 映射共享区域:如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
- 设置程序计数器:execve做的末了一件事就是设置当进步程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
图-41 缺页异常处理
缺页故障:一个虚拟页没被缓存在DRAM中,即DRAM缓存不掷中被称为缺页。当CPU引用了一个页表条目中的一个字,而该页表条目并未被缓存在DRAM中,地址翻译硬件从内存中读取该页表条目,从有效位为0可以判定尚未被缓存,进而触发缺页异常。
缺页中断处理:
- 处理器生成一个虚拟地址,并将它传送给MMU
- MMU生成PTE地址,并从高速缓存/主存哀求得到它
- 高速缓存/主存向MMU返回PTE
- PTE中的有效位是0,所以MMU出发了一次异常,通报CPU中的控制到利用系统内核中的缺页异常处理程序。
- 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
- 缺页处理程序页面调入新的页面,并更新内存中的PTE
- 缺页处理程序返回到原来的历程,再次实行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会掷中。
7.9动态存储分配管理
动态储存分配管理使用动态内存分配器(如malloc)来进行。动态内存分配器维护着一个历程的虚拟内存区域,称为堆。分配器将堆视为一组差别大小的块的集合。每个块就是一个连续的虚拟内存页,要么是已分配的,要么是空闲的。
已分配的块显式地保留从而为供应应用程序使用。空闲块可以用来分配,空闲块在被应用分配之前都会保持空闲状态,而已分配的块在被释放之前都会保持已分配状态。释放要么由应用程序显式实行,要么是内存分配器自身隐式实行的。其中动态内存分配主要有两种基本方法与计谋:
- 显示分配器:要求应用显式地释放任何已分配的块
- 隐式分配器:要求分配器监测一个已分配块何时不再被应用程序所使用。并在不被使用时释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用地已分配的块的过程叫做垃圾收集。
7.10本章小结
本章主要围绕存hello的存储管理进行展开,其中详细先容了储器地址空间、段式管理、页式管理,VA 到 PA 的变更、物理内存访问,hello历程fork时和execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容;
至此,我们对于hello的了解已经较为全面,在接下来的一章我们将讲述我们编写hello最初的假想也是hello最为核心的任务与任务,即:I/O交互。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
在lunix系统中,全部的I/O设备都被模型化为文件,而全部的输入和输出都被当尴尬刁难相应文件的读和写来实行,这种将设备优雅地映射为文件的方式,答应Linux内核引出一个简单低级的应用接口,称为Unix I/O。
一个Linux文件就是一个m个字节的序列,这种将设备映射为文件的方式,答应Linux内核引出一个简单、低级的应用接口,使得全部的输入和输出都能够以一种统一且一致的方式来实行。
8.2 简述Unix IO接口及其函数
8.2.1.Unix I/O接口
(1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的全部利用中标识这个文件。内核记载有关这个打开文件的全部信息。应用程序只需记着这个描述符。
(2)读写文件:一个读利用就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增长到k+n。给定一个大小为m字节的文件,当k>=m时实行读利用会触发一个称为EOF的条件,应用程序能检测到这个条件。雷同的。写利用就是从内存复制n>0个字节到从内存复制n个字节文件,从当前文件位置k开始,然后更新k。
(3)改变当前的文件位置:对每个打开的文件,内核保持着一个文件位置k,初始位置为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能通过seek利用,显示地设置文件的当前位置为k。
(4)关闭文件:当应用完成对文件的访问之后,它就关照内核关闭这个文件。作为相应,内核释放文件打开时创建的数据结构,并将这个描述符规复到可用的描述符池中。无论一个历程因为何种原因停止时,内核都会关闭全部打开的文件并释放它们的内存资源。
8.2.2.Unix I/O函数
(1)int open(char *filename, int flags, mode_t mode);
历程通过调用open函数打开一个已存在的文件或者创建一个新文件:
Open函数将filename转换为一个文件描述符,而且返回描述符数字。返回的描述符总是在历程中当前没有打开的最小描述符。flags参数指明了历程计划怎样访问这个文件:O_RDONLY:只读、O_WRONLY:只写和O_RDWR可读可写。mode参数指定了新文件的访问权限位。
(2)int close(fd):
历程调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。
(3)ssize_t read(int fd, void *buf, size_t n);
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数目。
(4)ssize_t write(int fd, const void *buf, size_t n);
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
(5)off_t lseek(int fd, off_t offset, int whence);
Lseek函数用于修改文件偏移量,其中fd为文件描述符,offset为需要偏移的字节数,whence为开始的位置:宏界说SEEK_END,SEEK_CUR,SEEK_SET分别为文件末了、当前位置、文件开头。
8.3 printf的实现分析
printf函数的函数体如图42所示
图-42 printf函数的函数体
va_list是一个字符指针的重界说,即:typedef char* va_list,(char*)((&fmt) + 4 )是第一个参数,这与栈的结构有关,*fmt存放在栈中,后续的字符型指针也都存在栈中,而指针大小位四个字节,所以+4得到第一个参数。注:此时的4字节应该是32位的linux系统,我们使用的是m64编译命令,此时一个指针大小8字节。
之后调用了vsprintf函数,其函数体如下图所示:
图-43 vsprintf函数的函数体
vsprintf函数将全部的参数内容格式化之后存入buf,返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照革新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数的函数体如图44所示
图-44 getchar函数的函数体
程序调用getchar时,程序开始等待用户按键,用户所输入的字符被存放在键盘缓冲区中直到用户按回车(回车也同样被包罗在缓冲区内)。此时getchar开始通过系统调用read读取存储在键盘缓冲区的ASCII码。getchar函数的返回值是用户输入的第一个字符的ascii码,如堕落返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar函数调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。然后getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要先容了linux系统对于I/O设备的管理方法,并简单叙述了Unix I/O接口以及相应的函数,而且对于其中的printf与getchar函数的详细实现进行了分析。
至此我们已经了解了hello作为我们最初编写的“初恋代码”,完成我们所希望的最重要的功能的详细实现过程,而且回顾了hello从一个C语言编写的源代码文本文件程序,一步步变成一个可实行目标文件,进而被利用系统使用shell加载进入底层硬件,一步步完成他的任务,终极被系统回收的全部过程!至此hello的生命也便走到了尽头…………
结论
结论:
至此hello已经走完了他的一生的全部旅程,同时作为每个程序员的“初恋程序”我们借助hello对于计算机系统以及一个程序从编写到终极运行实现的全过程也有了更加深入的了解与体会,同时再次回顾hello所走过的一生:
- 编写hello.c的源程序,完成hello.c文件;
- 预处理:调用的库以及宏界说与源文本文件更换归并得到hello.i文本文件;
- 编译:将文本文件hello.i编译生成汇编语言文件hello.s;
- 汇编:将汇编语言文件hello.s汇编得到二进制可重定位目标文件hello.o;
- 链接:hello.o与其它调用库函数地点的可重定位目标文件和动态链接库链接生成可实行文件hello,至此hello可以被加载入内存并运行;
- 创建历程:终端shell调用fork函数,创建一个子历程,为程序的加载运行提供虚拟内存空间等上下文;
- 加载程序:终端shell调用execve函数,启动加载器映射虚拟内存,之后开始载入物理内存,在进入main函数;
- 访问内存:通过TLB和多级页表,实现虚拟内存和物理内存的翻译,进而访问计算机的存储结构,访问内存;
- IO:hello输入输出与外界进行交互;
- 停止:终极hello被父历程回收,内核收回为其创建的全部信息。
感悟:
计算机系统的运行远远比我们实际所看到的与想到的要复杂的多,其中凝结了无数计算机范畴先辈的聪明与心血,系统使用极其精妙的构思设计完善整合了软件与硬件,使得建立在该系统之上的一切上层建筑可以更加牢固更加高效的运行;回顾了hello的一生,我学到了计算机科学的世界,一切看似简单的利用与设计,背后都极有可能蕴藏着巨大精妙的构思,因此在将来的学习中我还应当戒骄戒躁,潜心刻苦努力钻研,止于至善!
附件
- 1. hello.c:源程序文件
- 2. hello.i:hello.c预处理后的源程序文件
- 3. hello.s:hello.i编译后的汇编程序
- 4. hello.o:hello.s汇编后的可重定位目标文件
- 5. elf.txt:hello.o的ELF文件
- 6. dump_hello.txt:hello.o的反汇编代码
- 7. hello:hello.o链接后的可实行目标文件(不包罗可调试选项)
- 8. hello1:hello.o链接后的可实行目标文件(包罗可调试选项)
- 9. hello_objdump.s:hello的反汇编代码
参考文献
[1] Markmap. https://ysyx.oscc.cc/slides/hello-x86.html
[2] 忆梦初心 C语言学习之预处理. 2023-07-22 https://blog.csdn.net/m0_69909682/
article/details/128616065
[3] 月光下的麦克 readelf指令使用.2023-0201 http://t.csdnimg.cn/mpcVG
[4] 长路漫漫2021 Shell和Bash的区别和接洽 http://t.csdnimg.cn/VXp25
[5] Pianistx printf函数的深入分析https://www.cnblogs.com/pianist/p/3315801.
html
[6] Randal E. Bryant;David R.O’Hallaron. 深入明确计算机系统.背景:呆板工业出书社,2016.7
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |