hello的一生
第1章 概述
1.1 Hello简介
在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork,产生子进程,于是hello便从Program摇身一变成为Process,这便是P2P的过程。
之后shell为其execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,以上全部便是020的过程。
1.2 环境与工具
硬件环境:X64 CPU 8G Hz 8G RAM 1T HDD
软件环境:Win10 64位 VMware 14 Ubuntu 18.04 64位
使用工具:code blocks gdb objdump gcc edb
readelf
1.3 中间结果
hello.c:源程序文件
hello.i:预处理后的文本文件
hello.s:编译后的汇编文件
hello.o:汇编后的可重定位文件
hello.elf:hello的ELF格式文件
hello:链接后的可执行文件
1.4 本章小结
本章主要简单介绍了hello的p2p,020过程,列出了本次实验信息:环境、中间结果。
第2章 预处理
2.1 预处理的概念与作用
概念:
预编译器(cpp)根据以字符#开头的命令,将头文件中的内容和宏定义直接插入到Hello.c文件中,最终的得到一个以i为扩展名的C文件—Hello.i文件。
作用:
使编译器在对程序进行翻译的时候更加方便。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
头文件stdio.h stdlib.h unistd.h中包含的文件被插入到相应位置,并且还有一些用到的函数的声明,方便编译器将其翻译为汇编语言。
2.4 本章小结
本章节简单介绍了c语言在编译前的预处理过程,简单介绍了预处理过程的概念和作用,对预处理过程进行演示。
第3章 编译
3.1 编译的概念与作用
概念:
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
作用:
将预处理后的.i文件经过分析优化后生成相应的汇编代码文件。
3.2 在Ubuntu下编译的命令
3.3.1 变量:
int sleepsecs
3.3.2 赋值:
int sleepsecs
= 2.5
i = 0
3.3.3类型转换:
变量sleepsecs是int型的,而2.5是float型的,所以在对sleepsecs进行赋值的时候还有一个隐式的类型转换,将2.5转换为2再赋值给sleepsecs。
3.3.4算术运算
加法:
3.3.5关系运算:
小于:
不等于:
3.3.6函数中的参数传递:
对于函数来说,通过寄存器或者栈来传递参数,最多可以通过寄存器传递6个参数,并且在64位汇编中,第一个参数放在%rdi,第二个放在%rsi,第三个%rdx,第四个%rcx,第五个%r8,第六个%r9,其余的应该通过栈传递,而这里我们只有两个参数,argc放在rdi中,argv放在rsi中。我们在函数分配栈空间后,利用mov指令把这个两个参数传送到函数的堆栈段。
3.3.7控制转移:
if:
for:
3.3.8函数操作:
printf(“Usage: Hello 学号 姓名!\n”);
printf(“Hello %s %s\n”,argv[1],argv[2]);
sleep(sleepsecs);
3.4 本章小结
汇编代码与我们的高级语言已有了很大的不同,里面涉及到了很多如寄存器等真正在计算机上如何实现的过程,基本上是计算机真正如何执行我们的程序,可能一个简单的for循环,if语句,函数调用,在汇编语言中会花费比高级语言多的多的语句来实现,就是一个简单的hello程序,汇编代码就花费了几百行来实现。幸运的是我们不用自己编写汇编代码,不过懂得汇编代码如何运行对于我们对程序的理解和以后面对一些在高级语言中难以发现的错误都大有脾益。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编器(as)
将hello.s翻译成机器语言指令,把这些指令打包成一 种叫做可重定位目标程序(relocatable object program) 的格式,并将 结果保存在目标文件hello.o 中。
作用:
通过汇编这个过程,把汇编代码转化成了计算机完全能够理解的机器 代码,这个代码也是我们程序在计算机中表示。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
ELF Header:用于总的描述ELF文件各个信息的段。
节头:描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息
符号表:包含m的定义和引用的符号的信息。
重定位节:
4.4 Hello.o的结果解析
hello.o反汇编:
hello.s:
我们可以看出跟hello.s相比,hello.o反汇编之后虽然右边的汇编代码没有太大的差别(多了一些跳转位置的解释和注释),但是左边多了一大堆东西。在冒号前面的是运行时候的机器指令的位置,冒号后面的是每一行汇编语句所对应的机器指令啦。机器语言是完全由0/1构成的,在这里显示的时候表示成十六进制的
在hello.s中跳转到的目标位置都是用.L3/.L4来表示的,在hello.o反汇编之后,这些目标被用具体的地址位置代替。在原先的hello.s中,调用一个函数只需被表示成call+函数名,但是在hello.o反汇编的结果中我们可以看见,这里的call是call一个具体的地址位置。
4.5 本章小结
本章简述了hello.s汇编指令被转换成hello.o机器指令的过程,通过readelf查看hello.o的ELF、反汇编的方式查看了hello.o反汇编的内容,比较其与hello.s之间的差别。学习了汇编指令映射到机器指令的具体方式。
第5章 链接
5.1 链接的概念与作用
概念:
将多个文件拼接合并成一个可执行文件的过程。
作用:
降低了模块化编程的难度
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
ELF头:
节头:
符号表:
重定位节:
5.4 hello的虚拟地址空间
用edb打开hello,可以在Data
Dump窗口看见hello加载到虚拟地址中的状况:
可以看出程序是在0x00400000地址开始加载的,结束的地址大约是0x00400fff
5.5 链接的重定位过程分析
hello.o:
hello:
由hello.o和hello的比较可以看出hello.o从text节开始,hello从init节开始,hello.o中的相对偏移地址到了hello中变成了虚拟内存地址,hello中相对hello.o增加了许多的外部链接来的函数,hello相对hello.o多了很多的节类似于.init,.plt等,hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。
重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的输入模块的.data节被全部合并成一个节,这个节成为hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
5.6 hello的执行流程
程序名称 | 程序地址 |
---|---|
ld-2.27.so!_dl_start | 0x7fce 8cc38ea0 |
ld-2.27.so!_dl_init | 0x7fce 8cc47630 |
hello!_start | 0x400500 |
libc-2.27.so!__libc_start_main | 0x7fce 8c867ab0 |
-libc-2.27.so!__cxa_atexit | 0x7fce 8c889430 |
-libc-2.27.so!__libc_csu_init | 0x4005c0 |
hello!_init | 0x400488 |
libc-2.27.so!_setjmp | 0x7fce 8c884c10 |
-libc-2.27.so!_sigsetjmp | 0x7fce 8c884b70 |
–libc-2.27.so!__sigjmp_save | 0x7fce 8c884bd0 |
hello!main | 0x400532 |
hello!puts@plt | 0x4004b0 |
hello!exit@plt | 0x4004e0 |
*hello!printf@plt | – |
*hello!sleep@plt | – |
*hello!getchar@plt | – |
ld-2.27.so!_dl_runtime_resolve_xsave | 0x7fce 8cc4e680 |
-ld-2.27.so!_dl_fixup | 0x7fce 8cc46df0 |
–ld-2.27.so!_dl_lookup_symbol_x | 0x7fce 8cc420b0 |
libc-2.27.so!exit | 0x7fce 8c889128 |
5.7 Hello的动态链接分析
在edb调试之后我们发现原先0x00600a10开始的global_offset表是全0的状态,在执行过_dl_init之后被赋上了相应的偏移量的值。这说明dl_init操作是给程序赋上当前执行的内存地址偏移量,这是初始化hello程序的一步。
5.8 本章小结
本章介绍了链接的概念和作用,分析了hello的ELF格式,虚拟地址空间的分配,重定位和执行过程还有动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是计算机程序需要进行对数据集合进行操作所运行的一次活动
作用:
每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
6.2 简述壳Shell-bash的作用与处理流程
作用:
shell 是一个交互型的应用级程序,它代表用户运行其他程序。
处理流程:
shell 执行一系列的读/求值(read /evaluate ) 步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
6.3 Hello的fork进程创建过程
Shell通过调用fork 函数创建一个新的运行的子进程。也就是Hello程序,Hello进程几乎但不完全与Shell相同。Hello进程得到与Shell用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。Hello进程还获得与Shell任何打开文件描述符相同的副本,这就意味着当Shell调用fork 时,Hello可以读写Shell中打开的任何文件。Sehll和Hello进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程
execve 函数加载并运行可执行目标文件filename,
且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。
6.5 Hello的进程执行
Linux 系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当shell
运行一个程序时,父shell 进程生成一个子进程,它是父进程的一个复制。子进程通过execve 系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段袚初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。
6.6 hello的异常与信号处理
ctrl+z:
ps:
这个操作向进程发送了一个sigtstp信号,让进程暂时挂起,输入ps命令符可以发现hello进程还没有被关闭。
jobs:
jobs可以查看当前的关键命令
pstree:
pstree是用进程树的方法把各个进程用树状图的方式连接起来
fg:
fg命令可以使后台挂起的进程继续运行,上面输出了三次,这里再继续输出七次
kill:
kill命令可以发送SIGKILL信号给指定的pid杀死进程
乱按:
由图可知,乱按只是将屏幕的输入缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),其他字串会当做shell命令行输入。
6.7本章小结
本章介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁。讲述了shell的基本操作以及各种内核信号和命令,还总结了shell是如何fork新建子进程、execve如何执行进程、hello进程如何在内核和前端中反复跳跃运行的。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址(LogicalAddress)是指由程序产生的与段相关的偏移地址部分。就是hello.o里面的相对偏移地址。
线性地址:地址空间(address space) 是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(linear address space) 。就是hello里面的虚拟内存地址。
虚拟地址:CPU 通过生成一个虚拟地址(Virtual
Address, VA) 。就是hello里面的虚拟内存地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。计算机系统的主存被组织成一个由M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。就是hello在运行时虚拟内存地址对应的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如下图:
其中Base字段,它描述了一个段的开始位置的线性地址,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中,由段选择符中的T1字段表示选择使用哪个,=0,表示用GDT,=1表示用LDT。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。如下图:
转换的具体步骤:
-
给定一个完整的逻辑地址[段选择符:段内偏移地址]。
-
看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。可以得到一个数组。
-
取出段选择符中前13位,在数组中查找到对应的段描述符,得到Base,也就是基地址。
-
线性地址 = Base + offset。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[220]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。这里注意到,这个total_page数组有220个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。如图:
由上图可得:
1.分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
2.每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中
3.每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
依据以下步骤进行转换:
1.从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器)。
2.根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3.根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址。
4.将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
上图给出了Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。
7.5 三级Cache支持下的物理内存访问
首先我们要先将高速缓存与地址翻译结合起来,首先是CPU发出一个虚拟地址给TLB里面搜索,如果命中的话就直接先发送到L1cache里面,没有命中的话就先在页表里面找到以后再发送过去,到了L1里面以后,寻找物理地址又要检测是否命中,这里就是使用到我们的CPU的高速缓存机制了,通过这种机制再搭配上TLB就可以使得机器在翻译地址的时候的性能得以充分发挥。
三级Cache支持下的物理内存访问示意图
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图7.7 概括了私有区域的不同映射。
3.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
情况1:段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)
情况2:非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。
情况3:如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入新的页面并更新到页表。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
带边界标签的隐式空闲链表:
堆及堆中内存块的组织结构:
在内存块中增加4B的Header和4B的Footer,其中Header用于寻找下一个blcok,Footer用于寻找上一个block。Footer的设计是专门为了合并空闲块方便的。因为Header和Footer大小已知,所以我们利用Header和Footer中存放的块大小就可以寻找上下block。
隐式链表:
所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中Header和Footer中的block大小间接起到了前驱、后继指针的作用。
空闲块合并
因为有了Footer,所以我们可以方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变Header和Footer中的值就可以完成这一操作。
显示空间链表基本原理:
将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,如下图:
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
文件的类型:
1.普通文件(regular
file):包含任意数据的文件。
2.目录(directory):包含一组链接的文件,每个链接都将一个文件名映射到一个文件(他还有另一个名字叫做“文件夹”)。
3.套接字(socket):用来与另一个进程进行跨网络通信的文件
4.命名通道
5.符号链接
6.字符和块设备
设备管理:unix io接口
1.打开和关闭文件
2.读取和写入文件
3.改变当前文件的位置
8.2 简述Unix
IO接口及其函数
打开和关闭文件:
- open()函数:这个函数会打开一个已经存在的文件或者创建一个新的文件
2.close()函数:这个函数会关闭一个打开的文件
读取和写入文件:
-
read()函数:这个函数会从当前文件位置复制字节到内存位置
-
write()函数:这个函数从内存复制字节到当前文件位置
8.3 printf的实现分析
printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。
上面是printf的代码,我们可以发现,他调用了两个外部函数,一个是vsprintf,还有一个是write。
从上面vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
write函数是将buf中的i个元素写到终端的函数。
Printf的运行过程:
从vsprintf生成显示信息,显示信息传送到write系统函数,write函数再陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序。从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
可以看出,这里面的getchar调用了一个read函数,这个read函数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。我们可以发现,如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回
8.5本章小结
本章节讲述了一下linux的I/O设备管理机制,了解了开、关、读、写、转移文件的接口及相关函数,简单分析了printf和getchar函数的实现方法以及操作过程。
结论
hello从出生到死亡的过程:
-
编写:通过文本编辑器将代码敲到hello.c文件中
-
预处理:将hello.c用到的外部库合并到hello.i中
-
编译:将hello.i编译成为汇编文件hello.s
-
汇编:将hello.s会变成为可重定位目标文件hello.o
-
链接:将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
-
创建进程:在shell利用./hello运行hello程序,父进程通过fork函数为hello创建进程
-
加载程序:通过加载器,调用execve函数,删除原来的进程内容,加载我们现在进程的代码,数据等到进程自己的虚拟内存空间
-
执行指令:CPU取指令,顺序执行进程的逻辑控制流。这里CPU会给出一个虚拟地址,通过MMU从页表里得到物理地址, 在通过这个物理地址去cache或者内存里得到我们想要的信息
-
异常:程序执行过程中,如果从键盘输入Ctrl-C等命令,会给进程发送一个信号,然后通过信号处理函数对信号进行处理
10.结束:程序执行结束后,父进程回收子进程,内核删除为这个进程创建的所有数据结构
附件
hello.c:源程序文件
hello.i:预处理后的文本文件
hello.s:编译后的汇编文件
hello.o:汇编后的可重定位文件
hello.elf hello的elf格式文件
hello:链接后的可执行文件
参考文献
[1] C语言再学习—GCC编译过程:
https://blog.csdn.net/qq_29350001/article/details/53339861
[2] 深入理解计算机系统(1.1)------Hello World 是如何运行的:
http://www.cnblogs.com/ysocean/p/7497468.html
[3] 深入理解计算机系统(3.1)------汇编语言和机器语言:
https://www.cnblogs.com/ysocean/p/7580162.html
[4] LINUX 逻辑地址、线性地址、物理地址和虚拟地址 转:
https://www.cnblogs.com/zengkefu/p/5452792.html
[5] Linux内核中的printf实现:
https://blog.csdn.net/u012158332/article/details/78675427
[6] 内存地址转换与分段 :
https://blog.csdn.net/drshenlei/article/details/4261909
[7] 进程的睡眠、挂起和阻塞: