目录
摘 要
本篇大作业的主要内容是展示hello程序的一生。通过对hello.c文件从层层进行预处理、汇编、编译、链接生成可执行文件之后到进程被回收这个过程的探讨和研究,来对计算机系统的运行方式加以解释和说明。本文章主要针对程序的编译过程、进程管理进行研究和探讨。
关键词:计算机系统;编译;进程管理;
第1章 概述
1.1 Hello简介
P2P的含义:表示From Program to Process 从程序到可执行程序的过程。
P2P:hello.c文件最初为文本文件,经过cpp进行预处理 ,预处理器(CPP)根据以字符#开头的命令,修改原始的C程序。结果就得到了另一个C程序,通常是以.i作为文件扩展名。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。接下来,汇编器(as)将 hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件 hello.o中。最后通过链接需要的其他函数的.o文件通过链接器的功能生成可执行目标二进制程序。
图1.P2P的流程图
020的含义:From zero to zero 从创建到回收(是从0到0的过程)。
020:shell命令行输入命令./hello。Shell调用fork()函数创建子进程,再调用evecue程序来加载并运行hello程序。在程序运行结束时,会向父进程发送SLGCHLD信号,等待父进程对其进行回收。当hello被回收时,它的生命周期就结束了。
1.2 环境与工具
软件环境:ubuntu16.04;
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上;
开发工具:gcc; vim; edb;objdump.
1.3 中间结果
hello.c 源文件
hello.i 经过预处理的源程序
hello.s 经过编译以后的程序文件
hello.o 汇编器汇编后的可重定位文件
hello 程序文件
hello.elf 可重定位目标文件的ELF文件格式
hello.elf 可执行文件的ELF文件格式
1.4 本章小结
本章主要介绍了hello.c在编译过程中的运行方式,与hello的可执行文件执行和回收的过程,为接下来的细致讲解做了一个提纲挈领式的介绍,下面将依次每一个方面进行展开讨论。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
预处理的过程:预处理器(CPP)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include < stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
预处理的作用:预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。具体来说,预处理能够完成头文件的包含,将需要的文件插入到目标文件的指定位置等待编译生效;将宏里面的值与程序里面的用宏替换等。
2.2在Ubuntu下预处理的命令
使用命令gcc -m64 -no -pie -fno -PIC -E hello.c -o hello.i
图2.生成hello.i的截图
2.3 Hello的预处理结果解析
可以看到在预处理文件内前面包含头文件的预处理指令变成了大段头文件本身的内容,并递归地添加头文件中又包含的文件,直到编译所需的所有代码都被包含在内。文件添加了行号便于编译时分析。使得该文件相比于原来的C文件要大得多。我们可以观察到,在文件的末尾是我们写出的hello.c文件的内容,其余的内容都是为了方便编译过程而进行的必要增添操作。
图3.进行预处理后的hello.i文件
图4.在预处理过程后的hello.c文件位置和内容
2.4 本章小结
本章主要是对预处理部分进行了一定的阐述,在本章中,我们生成了预处理后的文件hello.i,这个文件比源文件要大很多。通过hello.i这一例子展示了预处理如何对待程序源代码,体现出预处理后的文件更加简单、直接、全面,有利于进一步编译,说明了预处理的好处和重要性。
第3章 编译
3.1 编译的概念与作用
C语言程序编译过程内的“编译”步骤,是指将.i形成的简单直接的c语言码经过分析并优化,翻译成一份汇编语言代码,得到的结果为.s文件。
编译的作用是把c语言翻译成汇编语言并进行优化,使其更加接近机器二进制语言,得到更简单明了和更符合机器运作规律的逻辑,方便下一步的汇编步骤。
3.2 在Ubuntu下编译的命令
使用的命令:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
图5.进行编译之后的hello.s文件
图6.1 hello.s文件内容(上)
图6.2 hello.s文件内容(下)
3.3 Hello的编译结果解析
3.3.1常量
该文件中的开头为只读代码段:
.file "hello.c"//表示文件名 .text//已编译程序的机器代码存放位置 .section .rodata //只读数据,比如printf语句中的字符串以及开关选项中的跳转表 .align 8 //表示空行 .LC0: //只读数据1 .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" .LC1: //只读数据2 .string "Hello %s %s\n" .text .globl main//表示函数的声明 .type main, @function//表示main的类型 |
.section 和.rodata段表明下面的数据为只读数据,里面的数值、函数的声明或是字符串都是在程序内部不会变化的。本程序的.rodata段中存储了两个字符串常量,即.LC0和.LC1里面的内容,分别对应源程序的两个printf。GCC编译器会默认把不带参数的printf ()优化替换成puts (),以提高运行速度。可以看到和源程序相比第一个字符串缺失了一个\n,这是编译器优化成用puts函数输出的结果。.rodata还可能存放const全局变量、switch跳转表等,但是在hello.c的内容中没有涉及。
3.3.2 变量
方由于hello.c文件内部没有全局变量的出现,所以下图是一个对于局部变量的表示,%rdi和%rsi中分别存放着输入的相关参数。汇编代码如图:
图7.使用变量的例子
以上方的变量使用为例,将函数的两个参数分别放入栈中,%edi表示第一个参数,即变量int argc;%rsi 表示第二个参数,即变量char ** argv(因为为地址所以占八个字节,使用%rsi来进行传递);同时使用了将数据放入内存分配出来的栈中的方式存储所得到的输入变量值。
3.3.3 赋值
图8.进行赋值的例子
此处为汇编代码中一个赋值的例子,表明的含义是将栈顶的元素取出并将其放置到%rax寄存器内部,即给这个寄存器赋值,之后对其进行更多的操作(如加减乘除)。
3.3.4 算数运算
下面是hello.c循环的一部分:
图9.进行算术运算的例子
从上面的算数运算可以看出这是一个加法操作,操作的对象是当前存在内存栈中的一个值,不断对其进行加法操作,直到这个值大于等于9时才不进行跳转,这个对一个值进行的不断的加法就属于一个算术运算。
3.3.5 关系操作
同样沿用图9的例子,关系的含义是指小于,小于等于,大于,大于等于和等于关系,是对两个操作数进行的比较,比如在图9中,将立即数8和放在-4(%rbp) 里面的值进行比较的过程就称为一个关系操作。这里与8的比较是一个编译器优化的结果,使用这个方式可以直接用cmp比较,能够提高效率;在计算机系统课中学到,进行比较的方法是使用条件码进行判断,使用三个条件码来判断这样可以简化cmp的实现。
3.3.6 函数操作
使用函数sleep()的实现汇编代码如下:
图10.调用sleep()函数
使用call来执行函数的调用,电脑的%rip能够通过call后面的相对位置找寻到sleep()函数的所在位置,将当前地址压入栈中后将rip放置在指定位置运行函数。
此汇编程序还调用了printf(),getchar()函数。
3.3.7 控制转移
正如图9所示,跳转指令为jle,能够将程序的pc进行移位,移位到规定的位置并将程序进行执行,直到下一次比较再一次进行跳转;这是一个循环结构中的跳转指令。另外,本程序中还有条件跳转的实现即if语句的实现,如图所示:
图11. if条件跳转指令
将获取到的参数与4进行比较,如果不等于4就跳转到if指令内部。
3.3.8 数组操作
数组在hello.s中也有体现,汇编语言中并没有数组的概念,而是将数组简化为其本质:一串相连的存储空间。要想访问数组中的某一个元素,就用数组的基地址加上偏移量来得到元素地址。
图12.数组访存过程
这里是访问数组的语句。第一条是获得数组argv的基地址,也就是argv[0],第二条在基地址上加了偏移量,第三条则访问了加偏移量后的地址,也就是argv[1],这里存储着输入的第一个字符串。之后的操作与之相同,同样访问了argv[2],这里存储着第二个字符串。
3.4 本章小结
在本章我们分析了在汇编后的文件的各个函数内元素的表示方式和执行过程,深入了解了在汇编代码层面上如何执行hello.c的程序,是用什么样的寄存器,如何传递参数,如何表示条件跳转,如何表示循环过程等一系列的程序实现问题。也为接下来的深入探讨做出了很重要的铺垫。
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言代码翻译成二进制机器语言,并生成可重定位目标文件的过程。汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。得到的二进制机器语言是机器可以直接理解并运行的,只要再经过链接就可以得到能够运行的完整程序了。
4.2 在Ubuntu下汇编的命令
命令为:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
图13.生成hello.i
4.3 可重定位目标elf格式
图14.将hello.o文件内的信息读入到txt文件
4.3.1 ELF头
ELF 头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。
图15.ELF头内的重要信息
4.3.2 节头部表
节头部表包含了不同节的类型,地址,偏移大小等基本信息。
图16.节头部表
4.3.3 程序头
该程序没有程序头
4.3.4 重定位条目
文件中有一些内存地址或引用,这些地方在链接前是待定的,需要视链接的情况指定确切的地址。因此,需要对这些地址进行重定位。每个代码段或数据段都对应一个重定位表,记录了段中的这些位置,方便对它们进行查找和操作。
图17.重定位条目
4.3.5符号表
.symtab是符号表,它列举了程序中用到的函数和全局变量。
图18.符号表
4.4 Hello.o的结果解析
使用命令:objdump -d -r hello.o >compare.txt
图19.反汇编结果
图19.部分汇编代码
现在我们来观察hello.s文件与反汇编的文件的不同之处:
1.使用的立即数从十进制转换到了十六进制
图19.反汇编代码段
使用上文中的图7对此进行比较,可以发现立即数表示一个为32,一个为20,而当前文件的使用时一个为0x20,一个为0x14,进制转换是为了能够将立即数转换为机器更能读懂的二进制表示方法。
2.控制转移的跳转指令的寻址方式发生变化
图20.反汇编代码段
在使用跳转指令时,使用图20的方式进行跳转而不是用.L2的方式来进行,使用相对位移来寻址的方式能够提高代码的可移植性和放置的随意性。
3.函数调用方式变化
图21.反汇编代码段
使用一个标签加偏移量而不是一个确切的地址或是一个函数名称来对函数进行调用,因为在生成可执行文件之前,代码段位置未知,所以在函数调用时也只能用偏移量来表示,符号的概念在机器内是无法阅读的,只能通过地址的偏移寻址进行。
4.5 本章小结
本章介绍了汇编的概念和作用,通过观察生成的ELF程序了解了在执行编译时使用的表的结构,同时也用反汇编工具对生成的汇编代码进行分析,认识理解到了汇编过程和编译过程中编译器做出的工作,也了解到了一些重要的重定位策略,对接下来的分析有所帮助。
第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
图22.进行链接
5.3 可执行目标文件hello的格式
图23.生成阅读elf的文本文件
和第四部分的分析相似,下面主要说不同之处:
ELF头:部分的表示方式发生了变化但是大部分内容相同,一些标志的参数发生了比较大的变化。
图24.ELF头内容
节头部表:节头部表的节的数量相较之前的.o文件增加了很多,增加了一些可执行文件所特有的段比如.init等,体现出了可执行文件与重定位文件的差异。.test为程序代码,.data是初始化的全局变量,.bss是未初始化的全局变量,.rodata是只读数据节,.symtab是符号节,.strtab是字符串节。
图25.节头部表内容
符号表:增添了一些需要的变量名与函数名,应该是与导入的库内部的一些内容。
图26.符号表内容
段节:
图27.段节内容
动态区域:
图28.Dynamic Section
重定位节:
图29.重定位节
5.4 hello的虚拟地址空间
Edb对hello进行加载的示意图如下:
图30.edb加载内容
图
图31.数据堆和有关表对应关系
观察数据堆的位置可以看到,程序的内存起始地址和.init的位置相同,均为401000位置,这是因为.init段在该虚拟地址的段最开始处出现,所以.init是程序最开始数据内容的地址;已经转换成机器代码的代码部分转载在.text文件内。
另外,观察运行前和运行后的栈和堆,可以看到栈的位置运行结束后如下
图32.1运行之前
图32.2运行之后
数据堆在运行前后如下所示:
图33.1运行之前
图33.2运行之后
5.5 链接的重定位过程分析
重定位:
重定位节和符号定位:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data 节被全部合并成一个节,这个节成为输出的可执行目标文件的.data 节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
图34 链接后的反汇编代码(部分)
二者的不同主要体现在以下几个方面:
- 反汇编call后面跟着的成了实际的地址;
- 反汇编多了许多新的节和函数,可以推测这些节是链接之后加入的。
重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。
5.6 hello的执行流程
运行调用的函数名(按照顺序给出) | 函数名对应的地址 |
<_init> | 401000 |
<.plt> | 401020 |
<puts@plt> | 401090 |
<printf@plt> | 4010a0 |
<getchar@plt> | 4010b0 |
<atoi@plt> | 4010c0 |
<exit@plt> | 4010d0 |
<sleep@plt> | 4010e0 |
<_start> | 4010f0 |
<_dl_relocate_static_pie> | 401120 |
<main> | 401125 |
<__libc_csu_init> | 4011c0 |
<__libc_csu_fini> | 401230 |
<_fini> | 401238 |
5.7 Hello的动态链接分析
动态链接:等到程序运行时才进行链接,它提高了程序的可扩展性(可作为为插件)和兼容性。动态链接的基本思想是把程序按照模块拆分成各个相对独立的部分,在程序运行时才将它们连接在一起形成完整的程序。我们找到.got.plt段,进行动态链接时会对之进行修改,我们将其放到edb运行,我们观察这一部分是否真的有修改。
图35.有关PLT数组相关信息
图36.1运行dl_init之前
图36.2运行dl_init之后
正如上方的前后对比可以看出:在运行dl_init前后,.plt段和.plt.sec段不会被修改。
下图是开始时没有进行dl_init的.got.plt的内存位置:
图37.1运行了dl_init前
这是运行了dl_init之后的:
图37.2运行dl_init之后
很明显进行了一定的改动,说明完成了动态链接过程。
5.8 本章小结
在本章中对链接的过程进行了拆分和细致的探索,通过查看ELF文件,对可执行文件的elf内容与第四部分的内容进行了比较,了解了可执行程序的特殊之处;观察hello的虚拟地址空间内容,了解到各个数据段的存放方式;同时分析了可重定位文件的相关内容,探索到可重定位的操作方式;最后通过edb工具,很细致的研究了在生成可执行文件的链接过程,尤其是使用动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程的一个经典定义就是一个执行中的程序实例。进程不仅包括代码,还包括当前的活动。
进程的作用:
进程提供给了我们假象,好像我们的程序是系统当前运行的唯一的程序一样,我们的程序好像是独占地使用内存和处理器,处理器就好像是无间断地一条一条执行我们的指令,我们的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell是你(用户)和Linux内核之间的接口程序。你在提示符下输入的每个命令都由shell先解释然后传给Linux内核。
shell 是一个命令语言解释器。拥有自己内建的 shell 命令集。此外,shell也能被系统中其他有效的Linux 实用程序和应用程序)所调用。
处理流程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(4)如果不是内部命令,调用fork( )创建新进程/子进程
(5)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(6)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait等待作业终止后返回。
(7)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数一般不返回,除非运行错误,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。它调用启动代码。启动代码设置
栈,并将控制传递给新程序的主函数。
6.4 Hello的execve过程
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数一般不返回,除非运行错误,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。它调用启动代码。启动代码设置
栈,并将控制传递给新程序的主函数。
6.5 Hello的进程执行
即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个 PC值的序列叫做逻辑控制流,或者简称逻辑流。 一个逻辑流的执行在时间与另一个流重叠,称为并发流, 这两个流被称为并发地运行。
用户模式和内核模式的切换:
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷人系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
进程时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
进程上下文:
内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
图38.进程时间片
6.6 hello的异常与信号处理
异常的种类有:中断、陷阱、故障、终止。
对于hello来说,这四种异常都是有可能的。比如中断可能是来自处理器外部的I/O设备的信号的的结果。对于陷阱,hello的exit以及sleep都会发生。对于故障,比如缺页故障。对于终止,比如硬件错误,DRAM和SRAM被损坏时发生的奇偶校验错误。
图39.异常的剖析
中断是异步发生的,是来自处理器外部的 I/O 设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。
图40.中断的剖析
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
图41.陷阱的剖析
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的 abort例程,abort例程会终止引起故障的应用程序。
图42.故障的剖析
终止是不可恢复的致命错误造成的结果, 通常是一些硬 件错误,比如 DRAM 或者SRAM 位被损坏时发生的奇偶错误。终止处理程序从不将 控制返回给应用程序。如图所示, 处理程序将控制返回给一个 abort 例程 ,该例程会终止这个应用程序。
图43.终止的剖析
信号:
图44.linux中的信号
运行程序:
图45.正常运行程序
输入ctrl+z:
图46.输入ctrl+z使进程挂起
输入ctrl+c:
图47.输入ctrl+c使进程结束
输入ps:
图48.输入ps查看子进程
输入pstree:
图49.展示进程树并找到该进程
输入jobs:
图50.展示作业
输入fg:
图51.继续进行前台程序
6.7本章小结
本章主要是讲了进程管理,主要包括了shell、异常、信号以及进程的创建和执行过程。对这一部分进行梳理,使得对hello程序的理解进一步提升。
结论
总结下来,hello的一生主要包括的内容有:
- 首先拥有了使用高级语言书写成的hello.c文件;这是一个程序的出生。
- 经过预处理,hello.c文件进行宏替换,并将已经调用的库中的函数添加到hello.c文件中,生成了更加完备的hello.i文件,此时还是使用高级语言进行的编写。
- 经过汇编阶段,hello.i内部的高级语言被翻译为汇编语言,同时使用许多的数据段来表示出不同的数据结构,为接下来的编译做出准备,生成了hello.s文件。
- 经过编译阶段,生成了hello.o文件(可重定位目标文件),此时的目标文件已经变成了机器可以理解的机器语言写成的文件,暂时没有放进一个虚拟地址内,做一个类比,现在已经是一个程序的半成熟阶段;现在的hello.o只需要等待链接器将它与其他需要的文件相链接。
- 经过链接阶段,生成了可执行程序hello文件,此时已经是hello程序的成熟阶段,此时的hello文件是由hello.o文件和其他可重定位的.o文件共同生成的。
- 在运行hello程序的过程中使用了shell,shell通过调用fork函数创建一个子进程并通过execve来运行hello这个程序,创建出一个hello可以进行执行的进程
- 为hello创建虚拟内存空间,并且把这个虚拟内存空间映射到物理内存当中.
- hello会在执行sleep函数的过程中发生一些异常(外部/内部信号导致的),此时会导致hello的上下文进行切换。
- 经过相关动态内存管理技术和IO设备管理技术之后,程序将被回收。
附件
列出所有的中间产物的文件名,并予以说明起作用。
中间产物文件名 | 作用 |
hello.i | 预处理后的文件 |
hello.s | 汇编操作后的文件 |
hello.o | 可重定位目标文件,用于执行链接 |
hello | 可执行目标文件 |
elf_o.elf | 可重定位文件的elf格式 |
Elf_out.elf | 可执行文件的elf格式 |
参考文献
[1] https://www.csdn.net/ csdn官网
[2]《深入理解计算机系统》,Bryant,R.E. ,机械工业出版社,2016.11.15