摘 要
本论文以hello程序的P2P及020过程,介绍程序编写、编译、加载、执行、回收的大致过程。其中每个过程在对应章节中有详细表述。使用Ubuntu对hello.c文件逐步操作得到中间产物和最终文件,并使用objdump查看机器代码的反汇编内容并进行详细分析。编写该论文,从头到尾梳理了hello程序P2P的整个过程,加深了自己对于程序的一系列操作的理解,也有利于读者学习程序的编写、编译、加载、执行、回收的大致过程。
关键词:预处理;汇编;编译;链接;进程管理;内存分配;信号处理;虚拟地址翻译;物理内存访问;堆空间管理;系统IO
第1章 概述
1.1 Hello简介
1-1 P2P的流程
1-2 O2O的流程
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:
1-3 硬件环境
软件环境:Microsoft Windows 10 (10.0) Home China 64-bit
Ubuntu 16.04
VMware Workstation Pro
开发调试工具:edb、VScode2017、gcc、code::blocks
1.4 本章小结
无论是复杂还是简单的程序,在编写后都需要进行预处理、编译、汇编、链接等一系列操作才能生成可执行文件。程序在运行时,操作系统为其分配内存空间,创建上下文,保证了程序的成功运行。cache、页表、TLB等为程序的运行加速。
第2章 预处理
2.1 预处理的概念与作用
预处理以字符#开头的命令,例如读入头文件,修改原始的C程序。预处理后hello.c文件被处理为hello.i文件,此时仍是文本文件。
2.2在Ubuntu下预处理的命令
2-1 ubuntu下预处理命令
2.3 Hello的预处理结果解析
hello.c预处理后生成修改后的源程序hello.i,其中以”#”开头的命令被该过程处理,例如导入了相应的.h文件等。可以看到,原来hello.c的代码部分被保留了下来。
2-2 预处理生成的hello.i文件
2.4 本章小结
预处理是hello.c程序到可执行程序的第一步,在该过程中以”#”开头的命令被优先处理,例如宏定义和头文件插入等。预处理生成了hello.i文件,为接下来的编译、汇编、链接操作做准备。
第3章 编译
3.1 编译的概念与作用
编译器将文本文件hello.i翻译成汇编语言的文本文件hello.s。汇编语言为不同的高级语言不同的编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
3-1 ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 向函数传递参数值、数组、指针
3-2 向main函数传递参数值的汇编代码部分
hello程序通过从命令行中读取参数到数组argv[]中,将参数的个数存放在argc中,进而向main函数传递参数。本节代码中,在执行main程序时首先将栈底指针入栈,同时令栈顶指针值等于栈底指针值,完成栈的初始化。然后通过减少栈顶指针值在栈中获得空间。此时的argc与argv[]存放在寄存器edi和rsi中,通过两个mov操作,将参数值进栈,从而完成了参数的传递。
3.3.2 控制转移(if)、关系操作
3-3 main函数中的if语句导致的跳转
完成参数传递后,main函数需要判断输入的参数个数是否为4,即通过argc值与4比较,如果相同则进行后续操作,在汇编代码中的体现为跳转到.L2。如果不相等,说明输入参数有错误,打印错误提示信息后退出程序。
3.3.3 控制转移(for)、算数操作、赋值、局部变量、函数调用、函数返回
3-4 main中for循环的具体实现
经过上个步骤,如果满足参数个数为4,则跳转到.L2。此时进入for循环。for循环的控制变量为局部变量i,且初值为0的i满足i<8时进入循环,步长为1。
.L2将i的初值赋值为0,跳转到.L3。循环开始前要将i与8进行比较,即执行.L3的第一条cmp语句,满足条件,进入循环体,在汇编代码中的体现为跳转到.L4。
在.L4中,调用了首先printf函数,通过main创建的栈为其传递参数到寄存器rdx和rsi中。接着调用sleep函数。注意到sleep函数中的参数为atoi函数,则首先需要执行atoi函数。仍是利用已经存储在栈中的argv数组,将所需值送入寄存器rdi,执行atoi后,返回值存储在寄存器eax中。再将其送入寄存器edi,则将参数值成功传入sleep中,完成sleep函数的调用。
此时,一次循环进行完成,将循环控制变量i的值+1,进入新一轮循环。
当i的值等于8时,循环停止。根据汇编代码,循环终止时调用getchar函数,并执行return指令,退出程序。
3.4 本章小结
编译是从源程序到可执行程序的第二步,在编译操作中,高级语言被转化成较低一级的汇编语言,便于后续将其转化成机器语言的操作。汇编后的文件仍为文本文件。通过理解汇编语言,可以建立高级语言实现的操作和硬件水平的操作的映射。
第4章 汇编
4.1 汇编的概念与作用
汇编器将hello.s翻译成机器语言指令,将其打包成可重定位目标程序,保存在hello.o中,此时的hello.o是二进制文件。
4.2 在Ubuntu下汇编的命令
4-1 Ubuntu下汇编的指令
4.3 可重定位目标elf格式
4-2 可重定位文件elf格式
4-3 hello.o的ELF头
从ELF头中我们可以得知hello.o文件的各种信息。可知hello.o是可重定位文件,节头大小为64字节,数量为13,字符串表索引节头为12。
4-4 hello.o的节头表
每个节的起始地址都为0,以便用于重定位操作。
4-5 hello.o的符号表
在可重定位文件中符号表存放可引用的全局符号和静态符号。在图中可以看出有各个函数,hello.o文件的各个节。
4.4 Hello.o的结果解析
4-6 hello.o反汇编后的内容
(hello.s文件的内容在前一节)
1.机器语言的构成,与汇编语言的映射关系:从总体流程看,hello.s与hello.o的总体流程相同。从hello.o的反汇编文件可以看出每行十六进制数代表着一个指令操作,存在一一对应的关系。机器语言由二进制数构成,分为操作数和操作码。
2.机器语言中的操作数与汇编语言不一致:每条01序列构成的指令都有不同的含义。例如,对于一条机器指令,有些序列代表寄存器的编号。将操作码与代表寄存器的序列结合后生成新的01序列,此时看来会有机器语言中的操作数与汇编语言不一致的现象。其本质是机器语言的指令结合了操作码。
3.分支转移:hello.s文件中使用标志来进行分支转移,如指令jmp .L2。而在hello.o反汇编得到的文件中,则采用了相对寻址方式进行分支转移操作。
4.函数调用:hello.o反汇编文件中调用函数的操作为“call 下条指令的地址”,在hello.s中则是“call 函数名”。因为hello.c中调用的函数都是库函数,在进行下一步链接后才能最终确定其地址,所以在汇编时将call指令后的地址设置为0,等待链接。
4.5 本章小结
hello.s通过汇编生成hello.o文件,文件类型从文本文件转换成二进制文件。汇编语言生成机器语言后,每行01序列代表着一行指令,存在一一对应关系。hello.o使用objdump进行反汇编后可以得到机器指令和汇编指令的对照。得到hello.o后,便可进行链接操作生成可执行文件。
第5章 链接
5.1 链接的概念与作用
链接是将关联的所有可重定位的目标文件结合到一起,形成一个具有统一地址空间的可执行目标文件的过程。
链接的作用:
- 将模块化编写的程序链接起来,成为一个整体,实现程序功能。
- 提高了空间利用率,源程序文件中无需包含共享库的所有代码,直接调用即可。可执行文件运行时的内存中只需要包含所调用的函数的代码。
5.2 在Ubuntu下链接的命令
5-1 Ubuntu下链接的指令
5.3 可执行目标文件hello的格式
5-2 hello的ELF头
5-3 hello的节头表
5-4 hello的符号表
由图5-3,hello是已经链接完成的可执行文件,则不存在地址缺省的情况。每个段的起始地址都对应着虚拟内存中的地址。可以直接从文件起始处得到各段的起始位置以及各段所占空间大小。
5.4 hello的虚拟地址空间
5-5 虚拟地址空间各段信息(部分)
从5.3可知,各段起始地址从0x400200到0x4006e0。可执行文件的连续的片被映射到连续的内存段。32位的程序头表中的每个表项用来描述一个节。
5-6 hello的程序头
Type描述存储段的类型或特殊节的类型。VirtAddr指出本段首字节的虚拟地址。PhysAddr指出本段首字节的物理地址。Align指出对齐方式,为2的整数幂。FileSiz指出本段在文件中所占的字节数。MemSiz指出本段在存储器中所占字节数。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5-7 比较hello.o(左侧)和hello(右侧)的反汇编代码
- hello.o经过链接后确定了该程序调用的各个函数的地址。
- hello多了_init的初始化部分。
- hello多了_dl_relocate_static_pie静态库链接的部分。
- hello多了__libc_csu_init 程序进行初始化。
- hello多了当程序正常终止时需要执行的代码部分_fini。
5-8 hello的重定位节信息
进行重定位时,链接器将所有的相同类型的节合并为同一类型的新的聚合节,将运行时的内存地址赋给每个节。完成操作后,程序中的每条指令和全局变量都有唯一的运行时内存地址。然后链接器需要修改代码节和数据节中对每个符号的引用,使其指向正确的运行时地址。
5.6 hello的执行流程
ld-2.27.so!_dl_start 0x00007ffe2052eef0
ld-2.27.so!_dl_init 0x00007f6d346bc630
libc-2.27.so!__libc_start_main 0x00007f6d342dcab0
[email protected] 0x0000000000400500
[email protected] 0x0000000000400520
[email protected] 0x0000000000400540
[email protected] 0x0000000000400510
libc-2.27.so!exit 0x00007f6d342fe120
5.7 Hello的动态链接分析
在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件hello。此时共享库中的代码和数据没有被合并到hello中。加载hello时,动态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。
动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。
5-9 调用init之前的.got.plt
5-10 调用init之后的.got.plt
从图中可以看到.got.plt的条目发生变化。
5.8 本章小结
通过链接器,将可重定位目标文件之间(或者与共享库)链接起来,生成了可执行文件。该文件可以在shell中执行,成为进程。链接时首先进行符号解析,然后将各个可重定位目标文件的节合并,确定内存地址,修改代码节和数据节的符号引用,使其指向正确的内存位置。静态库的链接相对于动态库较为简单。
第6章 hello进程管理
6.1 进程的概念与作用
进程是运行中的程序的实例。进程为程序的运行创建上下文,即为程序运行搭建环境。进程创造了一种假象,即程序独占地使用处理器和内存,并且不间断地执行其指令。
6.2 简述壳Shell-bash的作用与处理流程
shell 是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接
收用户输入的命令并把它送入内核去执行。shell 可以解析用户的命令并将其送入内核执行,同时 shell 允许用户编写 shell命令组成的程序。
shell 首先检查命令是否是内部命令,如果是则立即执行。若不是再检查是否
是一个应用程序。然后 shell 在搜索路径里寻找这些应用程序。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。
如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给 Linux
内核。
6.3 Hello的fork进程创建过程
向终端中输入命令行./hello 1180301022 张乙 1,shell对输入进行解析,解析发现hello不是一个内置的命令,则执行当前目录下的hello程序。首先会调用fork函数创建一个新的运行的子进程,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,但是拥有不同的PID。
6.4 Hello的execve过程
execve的功能是在当前进程的上下文中加载并运行一个新程序。在执行fork得到子进程后随即使用解析后的命令行参数调用execve,execve调用启动加载器来执行hello程序。加载器执行的操作是,加删除子进程现有的虚拟内存段,并创建新的代码、数据、堆和栈段。代码和数据段被初始化为hello的代码和数据。堆和栈被置空。然后加载器将PC指向hello程序的起始位置,即从下条指令开始执行hello程序。
6.5 Hello的进程执行
初始时,hello进程在用户模式中运行。
在第一次执行sleep时,hello的时间片结束,系统将控制切换到内核中,此时处于内核模式。完成了从hello到sleep的一次调度。完成sleep的执行,即休眠指定时间。
sleep调用完成后,系统恢复hello程序的上下文,控制返回hello程序,又切换到用户模式。完成从sleep到hello的调度。
程序之后重复上述操作,直到循环结束。
6.6 hello的异常与信号处理
1.运行过程中按下ctrl-C
运行时按下ctrl-C,hello进程终止,并向父进程发送SIGINT(进程终止)信号,由父进程负责完成子进程的回收。
6-1 运行时按下ctrl-C
2.运行过程中按下ctrl-Z
运行时按下ctrl-Z,子进程暂时挂起,向父进程发出SIGSTOP信号。此时子进程并不会被回收。当其收到特定信号时,会继续执行。执行结束后进程终止,向父进程发送SIGINT信号,被父进程回收。
6-2 运行过程中按下ctrl-Z
3.运行过程中乱按
运行过程中的无关输入被缓存到stdin,并随着printf指令被输出到结果。
6-3 运行时乱按
6.7本章小结
进程是一种非常成功的抽象,让程序执行时仿佛独占使用处理器和内存,方便了程序运行时的分析。Shell负责解释命令行并进行相应的操作。进程创建时,首先使用fork创建子进程,然后使用execve执行可执行目标程序。fork会创建出父进程的一份副本,但是父进程和子进程的PID不同。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:源代码经过预处理、编译、汇编后出现在汇编程序中地址。
线性地址:逻辑地址经过段机制,即综合标识符和偏移量得到的地址。
物理地址:真实的内存对应的地址。内存中,第一个字节为0,第二个字节为1…以此类推。CPU访问内存就是通过物理地址寻址的方式。
虚拟地址:虚拟地址经过MMU地址翻译可以得到物理地址。与线性地址相同。
7.2 Intel逻辑地址到线性地址的变换-段式管理
8086共设计了20位宽的地址总线,通过将段寄存器左移4位加上偏移地址得到20位地址,即逻辑地址。将内存分为不同的段,每个段有段寄存器对应,段寄存器有一个栈、一个代码、两个数据寄存器。
7.3 Hello的线性地址到物理地址的变换-页式管理
系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
7-1 单页表时的地址翻译过程
如图所示,MMU首先进行虚拟地址的解析,将高位虚拟页号(v*n)部分解析分成组索引和标记两部分,从首地址存放在页表基址寄存器(PTBR)的页表中寻找对应项。读取其物理页号作为物理地址的高位(标记)部分,然后将虚拟地址未被使用的低位与之结合,生成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
Core i7采用四级页表的层次结构。CPU产生VA,VA传送给MMU,MMU使用v*n高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,v*n1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。
7-2 Core i7地址翻译的概况
7.5 三级Cache支持下的物理内存访问
使用7.4环境中获得的PA,首先取组索引对应位,向L1cache中寻找对应组。如果存在,则比较标志位,并检查对应行的有效位是否为1。如果上述条件均满足则命中。否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后向上级cache返回直到L1cache。如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的原位置。
7-3 Core i7物理内存访问
7.6 hello进程fork时的内存映射
在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。
7.7 hello进程execve时的内存映射
execve调用启动加载器代码,在当前进程的上下文中加载并运行可执行目标文件hello。首先删除该进程中当前程序的内存区域结构。然后映射私有区域,即将栈、堆、.bss请求二进制0,.data和.text节由hello的相应节提供。然后对hello进行动态链接,将链接对象的.data、.text节映射到共享宽带内存映射区域。最后设置PC指向hello的入口地址。
7.8 缺页故障与缺页中断处理
如果程序执行过程中遇到了缺页故障,则内核调用缺页处理程序。处理程序会进行如下步骤:检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。然后检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。在两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
7-4 Linux缺页处理
7.9动态存储分配管理
堆由动态内存分配器维护。堆主要具有填充字、预言块、负载块、结尾块四个结构部分。各部分都具有自己的结构规则。每个负载块要么是已分配的,要么是空闲的。显式分配器要求程序显式地释放已分配块,隐式分配器则使用“垃圾收集”的策略隐式地回收不再使用的已分配块。
堆中的块主要组织为两种形式:
1.隐式空闲链表(带边界标记)
在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。
2.显式空闲链表
在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
7-5 使用双向空闲链表的堆块格式
块回收时需要调用块合并的操作。块合并通过修改header和footer的值进行。块合并时会遇到四种情况:前块和后块都不为空,前块为空后块不为空,后块为空前块不为空,前后块都为空。分别处理四种情况即可完成块合并操作。
7.10本章小结
磁盘上有一块连续的区域作为虚拟内存,虚拟内存作为磁盘到主存的cache。虚拟内存采用虚拟地址寻址的方式。虚拟地址的翻译过程是:通过虚拟地址的高位,在页表中找到物理内存的物理页号,再将其与物理内存偏移结合生成物理地址,此时便可以通过物理地址向内存中寻找对应内容。CPU通过虚拟地址寻址,经过翻译后得到物理地址。如果未在高速缓存和内存中找到对应位置,则触发缺页中断。经过缺页中断处理程序后,再次执行该指令即可命中。
堆采用动态存储空间管理的策略,申请目标大小的块,并在使用结束后释放它。释放时需要对相邻的空块进行合并(需要分情况处理),以免出现外部碎片降低内存效率。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux中所有的IO设备都被抽象成文件,输入和输出抽象为对文件的写和读操作。这种策略使Linux内核引出一个简单的接口(Unix I/O)。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
Unix I/O接口统一操作:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。头文件<unistd.h>定义了常量,可以替代描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
int open(char* filename,int flags,mode_t mode) 打开一个存在的文件或是创建一个新文件。
int close(fd),关闭目标文件。
ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置传送不超过n个字节到内存位置buf。
ssize_t wirte(int fd,const void *buf,size_t n),从内存位置buf复制不超过n个字节到当前文件位置。
8.3 printf的实现分析
printf的代码如下:
8-1 printf的函数体
其中参数列表中出现了”…”,代表着参数个数不确定。变量arg获得输出的时候格式化串对应的值。
vsprintf的代码如下:
8-2 vsprintf函数体
vsprintf的作用是格式化:它接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write的操作如下:
8-3 write进行的操作
执行write时,将栈中的参数放入寄存器,再通过系统调用syscall。
syscall:
8-4 syscall的操作
call save是为了保存中断前进程的状态。syscall将输入的参数(ACSII码)从寄存器传送到显卡的显存中。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,触发键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
Linux提供了一种简单使用的抽象——将系统的IO设备抽象成文件,系统的输入和输出被抽象成文件的写和读操作。在此基础上,Linux对系统IO的操作可以以打开文件、改变文件位置、读写文件、关闭文件的操作进行。同时分析了printf和getchar的实现。
结论
按照大作业的编写顺序,将hello经历的过程划分如下:
- 用户编写hello.c的源代码并保存。
- 对hello.c进行预处理操作,根据以“#”开头的命令,修改原始的C程序,得到hello.i。
- 对hello.i进行编译操作,将高级语言转化成汇编语言,得到hello.s。
- 对hello.s进行汇编操作,将汇编语言转化成机器语言(二进制),此时的hello.s被打包成可重定位目标程序,保存在hello.o中。
- 将hello.o与动态库进行链接操作,完成相应节的合并与符号的解析,确定程序运行时函数的真实地址。链接完成后得到可执行目标文件hello。
- 在shell中输入命令 ./hello 1180301022 zhangyi 8,shell对命令进行解析,并通过fork创建新进程。随后调用execve将hello程序加载到当前的上下文中,将PC设置为hello的第一条指令,开始执行hello。
- CPU为hello进程分配时间片,在时间片中hello进程仿佛独占地使用CUP和内存。当被调度到内核程序并返回到hello进程时,hello进程可以从终止的位置继续执行。
- 执行过程中,MMU将虚拟地址翻译成物理地址,并通过页表映射到内存,以访问内存。
- prinft在被调用时会通过malloc申请额外的堆空间。
- 执行结束后(可以是执行到程序末尾,也可以是键入ctrl-C信号),hello进程向父进程发送SIGINT信号,表明hello进程已经终止,此时父进程将子进程回收,内存空间得到释放。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c源代码
hello.c预处理得到的修改了的源程序
hello.i编译后生成的hello.s
hello.s汇编后生成的hello.o
hello.o使用objdump反汇编得到的结果
hello.o与动态库链接得到的hello可执行文件
使用objdump查看hello得到的结果
参考文献
[1] Randal E.Bryant/David R.O’Hallaron. 深入理解计算机系统. 北京:机械工业出版社,2016
[2] 朱利、李晨. 计算机系统结构. 北京:清华大学出版社,2012
[3] 张羽、黄小平. 计算机系统基础. 北京:清华大学出版社,2016
[4] printf 函数实现的深入剖析. https://www.cnblogs.com/pianist/p/3315801.html
[5] 内存虚拟地址的翻译和值读取. https://blog.csdn.net/chunyexiyu/article/details/51282204