计算机系统
题 目 程序人生-Hello’s P2P
专 业 未来技术人工智能模块
学 号 2022112257
班 级 WL021
学 生 董子宁
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文通过探索hello.c的一生,细致了解其从.C文件到执行到结束的过程,.C文件经过预处理,编译,汇编,链接成为可执行文件,在执行过程中又涉及进程管理,存储管理等内容,hello.c的生命历程将其串联在一起,我们将深入理解这一过程。
关键词:hello;P2P;020;汇编;链接;进程管理;存储管理
第1章 概述
1.1 Hello简介
运行hello.c程序时,是由编译器读取并启动程序,将程序转换为一个名为hello的可执行目标文件。第一步,cpp根据以字符#开头的命令修改hello.c,即预处理生成一个hello.i文件。第二步,ccl将hello.i文件编译,得到汇编语言程序的文件hello.s。第三步,as将hello.s翻译成机器语言指令,并将机器语言指令打包成可重定位目标文件hello.o。第四步,ld进行链接,得到可执行目标文件:hello。在shell中加载运行hello程序,系统会为hello创建(fork)一个子进程,这样,就完成了P2P的过程。
程序从无到有,形成可执行文件,在shell中产生子进程,通过execve加载hello,系统分配资源并执行。执行完成后,父进程将回收hello进程,内核将清除hello所有的痕迹,就完成了020的过程。
1.2 环境与工具
12代酷睿i7 CPU;2GHz;2G RAM;256GHD Disk 以上
Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 22.1 LTS 64位/优麒麟 64位;
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
hello.c:源程序
hello.i:预处理后的文件
hello.s:编译后汇编程序文件
hello.o:汇编后的可重定位目标文件
hello:链接后的可执行目标文件
elf.txt:ELF格式下的hello.o
1.4 本章小结
本章介绍了hello.c的P2P和020过程。介绍了实验的硬软件环境、开发和测试工具,以及中间结果文件。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理指的是程序在编译之前进行的处理,是计算机在处理一个程序时所进行的第一步处理,可以进行代码文本的替换工作,但是不做语法检查。 预处理是为编译做的准备工作,能够对源程序.c 文件中出现的以字符“#”开头的命令进行处理,包括宏定义# define、文件包含# include、条件编译# if def 等,最后将修改之后的文本进行保存,生成.i文件,预处理结束。
作用:预处理可以在在将c程序转化为s的汇编程序之前进行文本替换和宏扩展,方便后续的代码转化,并且对于在汇编中无用的注释进行处理,删去无用部分对后续操作做准备。
2.2在Ubuntu下预处理的命令
在终端中输入cpp hello.c > hello.i,得到预处理后的文件
2.3 Hello的预处理结果解析
图中包含hello.c中涉及到的所有头文件名称
除此之外
1.开头的文字注释被删除
2.头文件stdio.h、unist.h和stdlib.h都被替换成了相应的完整的库中的内容。
3.#+数字 代表编译器中原始代码的行号
hello.c的代码在文件最后
2.4 本章小结
预处理后的文件相较于源文件多了几千行,预处理器对hello.c进行了大量的展开操作,我们查看并比较了其中的差异。
第3章 编译
3.1 编译的概念与作用
概念:编译器将高级语言转换为汇编语言的过程,将hello.i翻译为hello.s
作用:将抽象的高级语言(C语言)翻译成汇编语言,同时gcc还会检查代码是否规范,是否有语法错误,编译器还能起到优化代码的作用。
3.2 在Ubuntu下编译的命令
gcc -S hello.c -o hello.s(使用ccl要进行下载,所以用该命令)
3.3 Hello的编译结果解析
3.3.1数据
常量:
字符串常量位于.rodata数据段。
根据UTF-8编码原则,汉字被编码为三个字节,其余字符与ASCII码规则相同,除汉字外字符保持原形式。
变量(全局/局部/静态):
局部变量有i,argc,argv[]
i先被初始化为零,每次迭代加一,与9比较,小于等于则继续循环
argc被放在了寄存器%edi中,然后放入栈中-20(%rbp)的位置,而argv的的地址放在了寄存器%rdi中,然后将数据放入栈中-32(%rbp)的位置。
3.3.2赋值
“=”赋值进行了一次
-4(%rbp)中储存的是i,即让i=0.
对于逗号操作符,只在函数的传参过程中出现,造成相邻的两次参数赋值
i在第一次执行时是没有被赋初值的,只有在argc第一次与5比较后,才被赋初值为0.
3.3.3类型转换
函数atoi将输入的字符串转换为int型
3.3.4 sizeof
以数组argv[]为例,内存中该数组占了32个字节的空间
3.3.5算数操作
只进行了i+1的操作
3.3.6 逻辑/位操作
该程序中不存在逻辑操作
3.3.7 关系操作
程序中有两处关系操作
第一处,5和变量argc的比较
第二处,循环变量i和9的比较,小于等于则继续循环,在机器语言中,小于等于则跳转至.L4.
3.3.8 数组/指针/结构操作
该程序中只有数组操作,且只有一个数组argv[],数组的地址被储存在栈中,一开始作为参数从%rsi转入栈中
从上图中可以看出数组的首地址存在栈中,寻址时,先将首地址放入寄存器%rax中,再分别加一定的偏移量,再从内存中取出相应的字符串。
3.3.9 控制转移
if的条件转移,argc=5则跳转至.L2
for循环中,i<=9则跳转至.L4
3.3.10 函数操作
main函数:
参数:argc,argv[],argc储存在%edi中,argv[]首地址储存在%rsi中
返回:主函数返回则程序结束,argc!=5时,call exit,退出,或是调用函数后,返回%rax中的值0.
printf函数和puts函数
参数:printf在for循环中调用时传入argv[1],argv[2],argv[3],puts无参数传入
调用:printf在for循环中调用,puts在if分支中
sleep函数
参数:转换为整型数的argv[4]
无返回值,作用是睡眠
exit函数
无参数传入
退出
3.4 本章小结
本章详细介绍了C语言程序转换为汇编语言后,程序的各个组成部分的表示,即C语言中的操作在汇编语言中如何体现,介绍了编译器如何处理C语言的各个数据类型以及各类操作。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器as将汇编语言的hello.s文件翻译成机器语言的可重定位目标文件hello.o的过程称为汇编
作用:汇编可以将汇编代码转换为计算机可执行的二进制代码,生成可重定位目标文件。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
4.3 可重定位目标elf格式
使用readelf命令,将elf结果保存到elf.txt中
- ELF头
ELF头有一个16字节的序列,该序列表示生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table) 的文件偏移,以及节头部表中条目的大小和数量。在上述表中,可以看出关于ELF header的长度这里也给出了,一共是64个字节。
- 节头表
描述了.o文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个条目。
本程序中没有程序头
- 重定位节
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,atoi, sleep,getchar。
重定位节包含了在代码中使用的一些外部变量等信息,在接下来的链接中,需要利用重定位节的信息修改一些变量符号,链接器会利用重定位节的信息,如偏移量等,计算外部变量符号的正确地址。
重定位节中各项符号的信息:
Offset偏移量:需要被修改的引用节的偏移
Info信息:包括符号和类型两个部分,符号在前面四个字节,类型在后面四个字节
Sym.Value符号值:标识被修改引用应该指向的符号,
Type类型:重定位的类型
Addend加数:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整
Sym.Name符号名称:重定向到的目标的名称。
文件中自动进行了翻译
- 符号表
符号表存放在程序中定义和引用的全局变量和函数等信息,在本程序中如getchar,puts,atoi等函数都存放在其中
4.4 Hello.o的结果解析
代码基本相同,但反汇编后的的每行前面都有一串16进制的编码。hello.s是由汇编语言组成的文件,反汇编得到的不仅有汇编语言,还有机器语言代码。
1.分支转移时,hello.s中是跳到如.L3等的代码段,给出的是代码段的名字,反汇编代码中,由于每一行都已经分配了地址,因此跳转命令后跟着的是要跳转部分的目标地址。
2.数字的进制不同:hello.s的数字用十进制表示,反汇编文件中数字是16进制的。
3.全局变量访问不同:在 hello.s 中,直接通过段名称加 %rip 寄存器访问 .rodata 段。然而在 hello.asm 中,初始阶段是不知道 .rodata 段的数据地址的,所以只能先写成 0(%rip) 进行访问。而在重定位和链接后,链接器会更新确定的地址以正确访问 .rodata 段中的数据。
4.5 本章小结
本章对汇编过程进行了详细的介绍,汇编处理将hello.s处理后生成一个可重定位目标文件hello.o,然后我们比较了hello.s和反汇编后的代码,展示了两者的区别。
第5章 链接
5.1 链接的概念与作用
概念:将一个或多个目标文件和库文件组合在一起,生成最终的可执行文件,这个文件可以被加载到内存并执行。
作用:通过将多个预编译的目标文件合并成为一个可执行文件,实现重定位,使分离编译成为可能。
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的格式
ELF头部表
节表
程序头
重定位表
符号表
5.4 hello的虚拟地址空间
使用命令edb –run hello
data dump显示虚拟地址从0x401000开始,到0x402000结束。与5.3对照,我们可以根据5.3中每一节对应的起始地址在edb中找到相应信息。
5.5 链接的重定位过程分析
输入命令objdump -d -r hello,得到可执行文件hello的反汇编结果。与hello.o的反汇编结果相比,hello的反汇编结果增加了很多函数代码,增加了程序执行main前的准备工作,以及一些库函数的定义。一些虚拟的地址操作数被设置成了真正的虚拟地址。
重定位节和符号定义链接器将相同类型的节合并,生成ELF节,形成可执行文件的虚拟空间,分配内存地址,再将重定位条目指向的位置设置为真实的地址。
5.6 hello的执行流程
0x401000 _init
0x401030 puts@plt
0x401040 printf@plt
0x401050 getchar@plt
0x401060 atoi@plt
0x401070 exit@plt
0x401080 sleep@plt
5.7 Hello的动态链接分析
动态链接中,编译器没有办法预测共享库函数的运行地址,因为共享模块在运行时会加载到任意位置。因此会为共享库函数的引用生成一条重定位记录,由动态链接器来解析。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
查看hello的ELF文件,发现GOT起始表位置:
执行init前
执行init后got.plt地址
5.8 本章小结
本章对链接过程进行了详细介绍,链接指令可将hello.o转换为可执行目标文件,分析了hello的格式,虚拟空间,链接的重定位过程,和hello的执行过程,并且分析了动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:计算机中正在运行的一个程序的实例,它是一个动态的实体,包含了程序代码以及程序执行时的当前活动,包括程序计数器、寄存器内容和变量。
作用:每个进程都有自己的文件描述,内存,地址空间等,操作系统通过进城来管理这些资源,使多个程序可以有效运行。
操作系统可以实现多个进程并发执行,提高系统的性能和效率。
6.2 简述壳Shell-bash的作用与处理流程
shell-bash是一个C语言程序,代表用户执行进程(一个命令行解释器)。它交互的解释和执行用户输入的命令,能够通过调用系统级的函数或功能执行程序、建立文件等。同时它还会协调程序间的运行冲突,保证程序能够以并行的形式高速执行。除此之外,bash还提供了一个图形化的界面,提高了交互的速度。
Shell的处理流程大致如下:
从Shell终端读入输入的命令。
切分输入字符串,获得并识别所有的参数
若输入参数为内置命令,则立即执行
若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
若输入参数非法,则返回错误信息
处理完当前参数后继续处理下一参数,直到处理完毕
6.3 Hello的fork进程创建过程
当Shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。它会加载指定的可执行文件,并接受参数列表和环境变量列表。execve只有在出现错误时才会返回到调用程序。与fork函数一次调用会返回两次不同,execve只会调用一次并且从不返回。
一旦可执行文件被加载,execve会启动启动代码。启动代码负责设置栈,将可执行文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的入口点或第一条指令来执行该程序,从而将控制权交给新程序的主函数。
6.5 Hello的进程执行
输入./hello,shell为hello fork一个子进程,execve函数在当前子进程的上下文中加载并运行hello,加载指定的可执行文件,并接受参数列表和环境变量列表,输出2022112257 dongzining 187****5007,执行sleep函数,休眠2s,内核转而执行其他进程,2s后回复hello的上下文,继续执行。
执行9次输出后,执行getchar函数,发生一个上下文切换,读取数据到缓存区后,发生中断,再次上下文切换执行hello进程。
6.6 hello的异常与信号处理
程序运行时使用CTRL+Z,进程收到SIGSTP信号,hello进程挂起并向父进程发送SIGCHLD.
ps查看程序进程状态
jobs查看已停止的程序
pstree,显示进程的分支图
调用kill杀死进程
按CTRL+C,向进程发送SIGINT信号,终止hello
异常种类:
中断:收到键盘输入 处理:处理器读取异常号,调用中断处理程序
陷阱:shell执行syscall fork一个进程 处理:陷阱处理程序在内核中执行后返回。
故障:缺页异常等 处理:从磁盘加载相应的页到主存。
终止:硬件损坏 处理:终止程序
6.7本章小结
本章详细介绍了进程,介绍了shell为hello fork进程,execve执行程序以及程序运行过程中异常的处理等,并探索了CTRL+Z后使用多种指令查看进程状态,以及向进程发送信号等。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序直接使用的,机器语言中用来指定一个操作数或一条指令的地址,由段和偏移量组成。
线性地址:由逻辑地址到物理地址变换之间的中间层。在分页机制下可以变换形成物理地址,不采用分页机制,直接就是物理地址。
虚拟地址:虚拟内存空间的地址,逻辑地址经过计算后得到的结果,需要经过MMU翻译后才能得到物理地址。hello中,反汇编代码中就是经过计算后的虚拟地址。
物理地址:计算机中真正的内存地址,每个字节都有自己的物理地址,物理地址决定了数据在内存中真正的存储位置。hello中寻找数据需要根据其物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由两个部分组成:段选择子和偏移量。段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。
段标识符:由16位长的字段组成,称为段选择符。其中,前13位是一个索引号,索引号是段描述符的索引,段描述符具体地址描述了一个段,段描述符数组称为段描述符表,因此可以通过段标识符的前13位在表中找到一个具体的段描述符,每个段描述符由8字节组成。后3位包括一些硬件细节,表示该段寄存器的属性。
段描述符的构成:
1.Base字段:表示包含段的首字节的线性地址,即一个段开始位置的线性地址;
2.全局段描述符:放在全局段描述符表(GDT中);
3.局部段描述符:放在局部段描述符表(LDT中)。
CPU将会通过其中的16位段选择子定位到GDT/LDT中的段描述符,通过这个段描述符得到段的基址,与段内偏移地址相加得到的64位整数就是线性地址。这就是CPU的段式管理机制,其中,段的划分,也就是GDT和LDT都是由操作系统内核控制的。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种操作系统内存管理技术,它将内存和进程的地址空间划分为固定大小的页,以实现对物理内存和逻辑地址之间的映射。其核心原理包括将主存储器分割为固定大小的页,并通过页表将线性地址映射到物理地址。此外,页目录存储指向页表的指针,页表将线性地址映射到物理地址,偏移量用于定位页内具体地址。处理器通过高位找到页目录项,中间位找到页表项,加上偏移量得到物理地址。TLB缓存用于加速地址翻译,存储最近的页表项以提高性能。整体而言,页式管理实现了高效的地址映射和内存管理,为多任务操作系统提供了良好的支持。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(Translation Lookaside Buffer)是一个硬件缓存,用于存储最近的一些虚拟地址到物理地址的映射。在四级页表结构中,虚拟地址通常划分为索引1、索引2、索引3和偏移量这四个部分,共同构成层级结构,用于访问页表。
当CPU需要将虚拟地址转换为物理地址时,首先会在TLB中查找映射。若在TLB中找到对应映射,则称为TLB命中,CPU可直接使用TLB中存储的物理地址;若未在TLB中找到映射,则称为TLB未命中。此时,CPU需要通过多级页表查找地址映射,先使用索引1找到第一级页表项,再使用索引2找到第二级页表项,依此类推,直至找到最终的页表项,并获取物理地址。
TLB的作用在于加速地址翻译的速度,因为其存储了最近使用过的虚拟地址到物理地址的映射,减少了对页表的频繁访问,从而提高了内存访问的效率。
7.5 三级Cache支持下的物理内存访问
为了提高 CPU的访存速度,计算机采用三级Cache缓存。低一级是高一级的缓存。
L1缓存:L1缓存是距离处理器核心最近的缓存,速度最快。当处理器需要访问内存中的数据时,首先会访问L1缓存。如果数据在L1缓存中找到了对应的数据,则可以立即访问这个数据,这被称为L1缓存命中。
L2缓存:如果在L1缓存中未找到需要的数据,处理器会继续检查L2缓存。L2缓存通常比L1缓存更大,但速度稍慢一些。如果数据在L2缓存中找到了对应的数据,则处理器会从L2缓存中读取这个数据,这称为L2缓存命中。
L3缓存:如果在L2缓存中也未找到需要的数据,处理器会继续检查L3缓存。L3缓存通常更大,但速度可能比L2缓存稍慢。如果数据在L3缓存中找到了对应的数据,则处理器会从L3缓存中读取这个数据,这称为L3缓存命中。
主存储器:如果在处理器的各级缓存中都未找到需要的数据,则处理器需要从主存储器中读取这个数据。这将导致一个内存访问周期,处理器从主存中读取数据并将其加载到适当的缓存层中。
这些缓存层级的存在可以极大地提高内存访问速度,因为较高级别的缓存通常速度更快,而且更接近处理器核心。只有在缓存中未命中时,才会导致对较慢的主存储器的访问。
7.6 hello进程fork时的内存映射
当shell使用fork创建子进程时,内核为新的子进程创建各种数据结构,并分配给子进程一个唯一的PID,为了给它创建虚拟内存空间,内核创建了当前进程的mm_struct、区域结构和页表的原样副本,将两个进程的页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
Execve函数在Shell中加载并运行名为hello的可执行目标文件中的程序,以hello程序有效地替代当前程序。该过程包括以下几个步骤:
1.清除已存在的用户区域。删除当前进程(Shell)虚拟地址空间中已存在的区域结构。
2.创建私有区域映射。为hello程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新区域均为私有且采用写时复制方式。代码和数据区域映射到hello文件中的.text和.data段。.bss段被映射为请求二进制零值,连接到匿名文件中,其大小信息包含在hello中。栈和堆区域也映射为请求二进制零值,且初始长度为零。
3.映射共享区域。若hello程序链接了共享对象(如标准C库libc.so),这些对象会被动态链接到该程序,并映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。执行execve的最后一步是设置当前进程上下文中的程序计数器,将其指向代码区域的入口点。下次调度hello进程时,程序将从该入口点开始执行。Linux会根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
缺页故障指DRAM缓存不命中,此时会触发缺页中断
- CPU转交控制权给操作系统内核,执行缺页中断处理程序。
- 操作系统分析引起缺页的原因
- 处理缺页:
未分配页面:如果地址对应的页面尚未分配,在MMU中注册相关页的物理地址。
页面被交换到磁盘:如果页面在磁盘上,则执行页面置换算法将一个页面从磁盘换入到物理内存中。
页面错误:如果是其他类型的页面错误,例如权限错误或无效访问,则相应地处理。
- 将相应的页放入物理内存。
- 恢复执行程序
7.9动态存储分配管理
动态内存分配器负责管理堆的分配和释放,维护内存的使用情况,并提供接口供程序员进行内存管理。堆是程序运行时用于动态分配内存的区域,动态分配器根据程序的请求在堆中分配内存空间,并在适当时机释放。动态内存分配器可以根据需要动态调整堆的大小,以便更有效地利用内存资源。对于内存的分配和释放,动态内存分配器可以分为两种类型:显式分配器和隐式分配器。
1)显式分配器:程序员显式地分配和释放内存。在这种情况下,程序员负责跟踪内存的分配和释放,并通过特定的API手动请求分配内存和释放不再使用的内存。
2)隐式分配器:由编程语言或者运行时环境自动处理内存的分配和释放。在这种情况下,内存的分配和释放是由语言或者环境的机制隐式完成的,程序员无需手动进行内存管理。例如,在一些高级语言中,有自动的垃圾回收机制,负责自动释放不再使用的内存。
7.10本章小结
本章详细介绍了hello的存储管理,intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。虚拟内存空间能够使得程序在表面上独占整个内存。
结论
- hello.c经过预处理后,库函数,宏定义的内容被插入到文本中,得到hello.o
- hello.o经过编译器编译变为汇编语言文件得到hello.s
- hello.s经过汇编器变成二进制可重定位目标文件hello.o
- hello.o经过链接器链接得到可执行文件hello
- shell为hello fork一个子进程,execve在子进程上下文中加载并运行hello
- hello在执行过程中可能遇到各种异常,有各种异常处理机制
- 在hello中的地址为虚拟地址,要经历虚拟地址映射为线性地址,再由线性地址计算得到物理地址。
- hello执行printf函数时会调用malloc向动态内存分配器申请堆中的内存
- hello执行完毕,由父进程回收终止的子进程,结束自己的一生。
附件
hello.c:源程序
hello.i:预处理后的文件
hello.s:编译后汇编程序文件
hello.o:汇编后的可重定位目标文件
hello:链接后的可执行目标文件
elf.txt:ELF格式下的hello.o
参考文献
[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] 深入理解计算机系统
[8]CSDNhttps://blog.csdn.net/daocaokafei/article/details/116207148?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165235562016781683965613%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165235562016781683965613&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-1-116207148-null-null.142^v9^pc_search_result_control_group,157^v4^new_style&utm_term=%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80&spm=1018.2226.3001.4187