//以下内容为个人搜集资料、查阅资料书而写,如有错误,欢迎指正评论
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 4 -
1.3 中间结果............................................................................... - 4 -
1.4 本章小结............................................................................... - 4 -
第2章 预处理............................................................................... - 5 -
2.1 预处理的概念与作用........................................................... - 5 -
2.2在Ubuntu下预处理的命令................................................ - 5 -
2.3 Hello的预处理结果解析.................................................... - 5 -
2.4 本章小结............................................................................... - 5 -
第3章 编译................................................................................... - 6 -
3.1 编译的概念与作用............................................................... - 6 -
3.2 在Ubuntu下编译的命令.................................................... - 6 -
3.3 Hello的编译结果解析........................................................ - 6 -
3.4 本章小结............................................................................... - 6 -
第4章 汇编................................................................................... - 7 -
4.1 汇编的概念与作用............................................................... - 7 -
4.2 在Ubuntu下汇编的命令.................................................... - 7 -
4.3 可重定位目标elf格式........................................................ - 7 -
4.4 Hello.o的结果解析............................................................. - 7 -
4.5 本章小结............................................................................... - 7 -
第5章 链接................................................................................... - 8 -
5.1 链接的概念与作用............................................................... - 8 -
5.2 在Ubuntu下链接的命令.................................................... - 8 -
5.3 可执行目标文件hello的格式........................................... - 8 -
5.4 hello的虚拟地址空间......................................................... - 8 -
5.5 链接的重定位过程分析....................................................... - 8 -
5.6 hello的执行流程................................................................. - 8 -
5.7 Hello的动态链接分析........................................................ - 8 -
5.8 本章小结............................................................................... - 9 -
第6章 hello进程管理.......................................................... - 10 -
6.1 进程的概念与作用............................................................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -
6.3 Hello的fork进程创建过程............................................ - 10 -
6.4 Hello的execve过程........................................................ - 10 -
6.5 Hello的进程执行.............................................................. - 10 -
6.6 hello的异常与信号处理................................................... - 10 -
6.7本章小结.............................................................................. - 10 -
第7章 hello的存储管理...................................................... - 11 -
7.1 hello的存储器地址空间................................................... - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换.................... - 11 -
7.5 三级Cache支持下的物理内存访问................................ - 11 -
7.6 hello进程fork时的内存映射......................................... - 11 -
7.7 hello进程execve时的内存映射..................................... - 11 -
7.8 缺页故障与缺页中断处理................................................. - 11 -
7.9动态存储分配管理.............................................................. - 11 -
7.10本章小结............................................................................ - 12 -
第8章 hello的IO管理....................................................... - 13 -
8.1 Linux的IO设备管理方法................................................. - 13 -
8.2 简述Unix IO接口及其函数.............................................. - 13 -
8.3 printf的实现分析.............................................................. - 13 -
8.4 getchar的实现分析.......................................................... - 13 -
8.5本章小结.............................................................................. - 13 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
P2P:首先程序员是用高级语言编写hello.c文件。接着通过预处理得到hello.i文件,这个文件中宏定义被展开,注释被删除,条件预编译语句被处理(如#if,#ifndef)……然后通过编译器将hello.c文件编译为hello.s文件,这个文件中高级语言被翻译为汇编语言,相当于机器执行的命令。接下来,通过汇编,汇编语言被翻译为机器语言,相应的产生hello.o文件。最后,将程序中调用的外部库函数等链接进来,产生可执行文件hello。通过bash创建子进程,execve()从而成为process。
020:bash程序通过fork函数创建一个新的子进程,获得一段新的虚拟内存,通过execve函数将可执行文件的信息转移到虚拟内存之中。通过页表结构,在运行中,应用缺页处理子程序将虚拟内存的信息加载到物理内存。程序接受虚拟内存被释放,页表复原。
1.2 环境与工具
软件环境:Ubuntu
硬件环境:intel I7 12700h
开发环境:VSCode
1.3 中间结果
1.Hello.c 源程序
2.hello.i 源程序预处理结构
3.hello.s 预处理文件转换为汇编语言
4.hello.o 可重定位目标文件
5.hello 可执行文件
6.obj_o.txt hello.o反汇编结果
7.obj_out.txt hello反汇编结果
1.4 本章小结
Hello的P2P、020在机器中都经历了复杂的过程。复杂的编译过程,复杂的执行过程。下面将分解介绍。
第2章 预处理
2.1 预处理的概念与作用
预处理就是将程序中,处理#开始的预处理语句。
作用
2.1.1条件编译
预处理会将程序中的条件编译语句处理掉,有选择性地保留或删除相应的 源程序。比如:
#ifdef CSAPP
#define NUM 1
#else
#define NUM 2
#endif
如果程序中已经定义了CSAPP那么NUM就会被定义为1,否则NUM被 定义为2.
2.1.2宏替换
预处理会将源程序中的宏定义替换为相应的东西。
比如#define x 4处理后,文本中的x会被替换为4。
比如#include “XXX.h”,那么对应的h文件中的内容会在该位置展开,将 该语句替换掉。
2.1.3处理空白字符(空格)
2.1.4 将注释删除,替换为空格
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
上两图是hello.i文件的部分内容
可见预处理已经将<stdio.h>,<unistd.h>,<stdlib.h>的内容替换到了文件之中
2.4 本章小结
预处理仅是将源程序代码进行了简单的替换,简化,方便了后续编译器的处理。
第3章 编译
3.1 编译的概念与作用
编译就是将预处理完的XXX.i的文件的内容通过分析处理转化为汇编语言
3.2 在Ubuntu下编译的命令
Hello.s文件的部分内容
3.3 Hello的编译结果解析
3.3.1函数操作
3.3.1.1参数传递
函数通过寄存器的特定位置来传递参数。
例如主函数参数argc,argv分别是通过edi,rsi传递的
3.3.1.2函数调用
通过call函数调用函数,提前将参数存储至寄存器
例如主函数调用exit(1)时:
3.3.1.3局部变量
函数的局部变量存储在内存的栈
例如函数中的局部变量i,存储在 -4(%rbp)中,循环控制变量初始化为零
3.3.1.4函数返回
函数返回通过 ret操作,返回值存储在rax中
3.3.2数据
常量
可见printf的参数以字符串的形式存储了起来
在调用printf函数时,将该字符串转移到rdi作为参数传入即可
其他数字常量,被直接引用
3.3.3赋值
在循环开始时,对循环控制变量i赋予了初值0,采用了movl指令
3.3.4数字操作
Hello.c源程序中仅涉及到了+操作,加操作可以通过add指令实现
循环过程中,循环控制变量i每次循环加一
3.3.5关系操作
3.3.5.1 !=
在程序中,首先一句argc参数判断用户执行程序时传入的参数数量。 在此执行了argc与4是否相等的操作。
3.3.5.2 <
循环控制条件时,i变量小于8.编译成果并未完全按照源代码的意思。 采用了判断i是否小于等于7来控制循环
其中jle指令的作用是,比较结果如果时小于等于那么跳转至 .L4段,或 者继续执行下一指令
3.3.6数组 指针操作
程序中调用了用户传入的参数argv[1],argv[2],且argv[]数组存储的是字符 串的首地址。
其中rax加0x16和0x8就是argv[1]和argv[2],但是这两个元素存储的是 字符串的地址,所以将rax的内容传给了寄存器,这样寄存器存储的就是字符 串的首地址
3.3.7控制转移
循环结构出出现了控制转移。
每次循环都要将i与7比较,如果小于等于7就跳转至循环内部,继续执 行,否则就继续执行下条指令
3.4 本章小结
编译过程就是将预处理过的程序文件hello.i中的内容转变为汇编语言
第4章 汇编
4.1 汇编的概念与作用
汇编就是将编译产生的汇编语言转换为机器码,产生的文件为二进制的可重定位的目标文件hello.o
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
从节头可以看见elf文件中的各节信息:.text, .rela.text, .data, .bss, .rodata, .comment, .symtab, .strtab, .shstrtab……
重定位节中,偏移量为符号相对重定位节首的偏移量。类型指的是重定位的引用使用32位PC相对地址。其中最重要的信息是,符号名称后的加数,这个数值直接关系链接时符号运行时的位置(一般来说这个位置,临时放进去的0x0,会被替代为ADDR(r.symbol) - (refaddr - r.addend),其中r.addend就是这个加数)。
4.4 Hello.o的结果解析
上图就是hello.o文件的反汇编结果。
可以看到,调用函数的地方已经预设为0x0,等待链接时计算运算时的地址。
汇编语言和机器语言的框架是相同的。汇编语言像是大致描述程序进程,但是机器语言就需要精确(有部分信息需要补全)的描述个步骤的准确信息,例如循环,汇编语言的”jle .L4”已经被替换为相对函数的位置了,更加精确了。
4.5 本章小结
汇编就是将编译所得的汇编语言文件.s文件转变为可重定位的目标文件hello.o
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程。链接就是将汇编生成的可重定位目标文件,与所需的库文件合并的过程,进而生成可执行文件(例如:程序中调用了printf函数,就需要将程序与printf.o文件链接起来)。
5.2 在Ubuntu下链接的命令
上图即为Ubuntu采用ld链接hello的方式。
其中所用的库文件可以通过查看gcc 编译信息来查看
其中的collect2就是调用的ld来实现链接功能
5.3 可执行目标文件hello的格式
5.3.0基本信息
上图为节头的部分内容。我们从节头我们就可以看出各节的基本信息。比 如某节的起始点就可以通过上面的地址(偏移量)求得。大小就表示这个节所 占空间大小。
5.3.1程序头
这个部分十分重要,建立了elf文件中各节向物理内存的映射。比如两个 LOAD就代表着程序的代码段和数据段,后面列出了二者对应的虚拟内存和物 理内存
5.3.2重定位段
重定位分析同4.3
5.3.3符号表
上图为elf文件中的符号表的部分内容。
我们知道符号包括函数和全局变量。Type信息就是表明这个符号是什么类 型。Value就是符号的值(函数就对应着地址)。
5.4 hello的虚拟地址空间
从edb查看的虚拟地址可以看到,与反汇编获得的机器语言可以对应上,也与readelf所得到的.text的起始地址相对应
5.5 链接的重定位过程分析
上图为hello.o文件的反汇编的部分结果
上图为可执行文件hello的反汇编的部分结果。
可以看到,二者在机器语言的内容基本一致,不同的只有对于可以重定位的地址不同。比如调用函数printf时,hello.o文件填写的地址内容为0x0,在可执行文件hello中就是0x401090,可知连接器已经将可重定位的地址定位完成。
举函数的例子来说重定位的过程,机器码中,对于函数的定位实际上是相对位置,相对于返回地址的位置。已知上面信息我们就好得到重定位的计算过程了,就是用函数的实际运行地址减去返回地址。比如printf函数,位于0x401090,返回地址是0x4011f9.所以我们得到这个相对位置为负的0x169转换为有符号整数就是0xfffffe97,所得就是图片中的97 fe ff ff。
5.6 hello的执行流程
程序先执行init子程序,跳转到main。
如果传入参数要求不符合要求,那么就先后调用printf和exit,地址分别为0x401203和0x401090
如果进入循环那么就会多次先后调用printf,atoi, sleep,地址分别为0x4010a0, 0x4010c0, 0x4010e0
循环结束后,调用getchar,地址为0x4010b0
5.7 Hello的动态链接分析
上面三个模块与动态链接相关,可以看见这些模块的相关信息,我们来观察执行前后节的内容变化。
上图就是init为执行前的内容
可以看见在执行init后,对应模块的内容发生了变化
动态链接是在程序执行的过程中实现的。
5.8 本章小结
链接就是将多个模块合并到一起,形成一个完整的软件程序,生成一个可执行文件。这种机制有利于多模块分开实现。
第6章 hello进程管理
6.1 进程的概念与作用
进程就是程序运行的抽象表现。这种数据结构,清晰的刻画了程序运行时的相关信息,有利于对于程序运行的观察、管理。
6.2 简述壳Shell-bash的作用与处理流程
Bash是一种应用程序,构建了用户于linux操作系统之间的交流桥梁。用户可以通过向bash输入的指令来对操作系统进行操作。
处理流程:
1.读取从键盘输入的命令
2.判断命令是否正确,且将命令行的参数改造为系统调用execve() 内部处理所要求的形式
3.终端进程调用fork() 来创建子进程,自身则用系统调用wait() 来等待子进程完成
4.当子进程运行时,它调用execve() 根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令
5.如果命令行末尾有后台命令符号& 终端进程不执行等待系统调用,而是立即发提示符,让用户输入下一条命令;如果命令末尾没有& 则终端进程要一直等待。当子进程完成处理后,向父进程报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。
6.3 Hello的fork进程创建过程
Shell接收到运行可执行文件hello时,会建立一个新的进程,再利用fork()函数来创建一个子程序。这个子程序除了pid与父进程不同,其他信息均与父进程相同。再把这个新建立的进程信息录入新建立的进程当中,shell还会让这个进程成为一个新的进程组的leader。
6.4 Hello的execve过程
从6.3可知在运行程序时,shell会建立一个新的进程。然而这个进程基本上与父进程相同。为了执行用户要求的程序,这个子进程会执行execve函数,将这个进程的所有信息(除了pid之外)更新为需要执行的程序的内容(比如代码段,数据段等)。在执行这个子进程时就会执行用户要求的程序了。
6.5 Hello的进程执行
切换进程时,CPU都会执行上下文切换。就是将CPU内核中保存的停止的进程的寄存器信息、虚拟内存等调出,恢复停止之前的状态。
CPU资源总归是有限的,为了让多个进程平均的利用CPU资源。每个进程执行时都会给予一个时间。进程时间片归零时,无论进程是否已经结束,都要被暂停,调入进程调度的程序。
用户态和核心态转换,与进程转换相似,都是通过上下文信息的切换实现的。
6.6 hello的异常与信号处理
6.6.1不停按空格、回车
可以看见并不会影响输出结果(如果忽视造成的格式变化),另外有意思 的是,因为没有清空缓存区,乱按的回车和空格还会被shell接受。
6.6.2 Ctrl-Z
按Ctrl-z后程序被中断,直接退出。
6.6.3 Ctrl-C
输入Ctrl-Z 后,作业被停止。
通过jobs和ps可以看到被停止的hello进程
在pstree所得的树形图中也可以看到被停止的hello进程
使用fg命令可以将hello进程调到前台继续运行
用kill命令可以给被停止的hello进程信号,上图杀死了这个进程
6.7本章小结
进程的管理是个很复杂的事情。CPU看似在同时做一件事情,其实它只是在快速的运行不同的进程。进程的切换就运用到了上下文信息。
在shell中有很多内置命令方便用户对于进程进行管理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址指段内的相对地址。
线性地址指段基址+逻辑地址。
物理地址要将线性地址通过地址转换机制转换为在内存上的地址。
其中逻辑地址、线性地址都属于虚拟地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址可以有段选择符和段内偏移量组成。段选择符区分这段地址是在LDT、GDT、IDT中。通过索引找到相应的段信息。然后通过基址寄存器或变址寄存器获取段内偏移。相加就可以的到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
对于内存页,采用了多级页表的管理方式。
每一级页表每个表项对应一个下一级,再通过线性地址中存储的页表索引找到该级的页表项,直至找到最终的内存页基址。最终根据线性地址中存储的偏移量找到相应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
现根据VA中的索引段在TLB中搜寻相应的索引,如果命中,直接获得PA的索引段。
如果没有命中,那么机器就会到四级页表中。索引段将会被解读为四分相同大小的四部分,分别代表在每一级页表的偏移段。如果未命中,那么机器先将对应的地址更新到内存中,再重新寻找。找到后TLB也会被修改。
经过上述部分得到的索引段与VA中的偏移段结合就是所得PA段。
7.5 三级Cache支持下的物理内存访问
在获得PA后,CPU会向存储传入读取信号。
接下来机器会从一级cache、二级cache等依次寻找直至找到后,向CPU返回相应的数据。
7.6 hello进程fork时的内存映射
调用fork后系统会给生成的子进程分配一块虚拟内存,将父进程所有的信息复制给子进程(除了PID)。也就是说父子进程共同使用一块物理内存。
但是如果子进程中发生了相应的数据变化,那么系统就会另开辟一段物理内存,并把相应更新部分的页表段也更新,指向新分配的物理内存。这块新分配的物理内存就存放修改的信息。
7.7 hello进程execve时的内存映射
Execve函数调用后,系统会删除进程所有用户部分中已存在的区域结构。
接下来系统会为代码段、数据段、共享库段分配新的区域,并将新程序的相应信息映射构建起来。另外堆栈等区域会申请一种匿名文件,内容全为二进制零(应该是为了防止堆栈错误产生不能挽回错误)。
这些内容其实还未被传入物理内存中,在真正执行程序触发缺页中断后才会被复制进物理内存。
7.8 缺页故障与缺页中断处理
如果触发缺页中断,系统会保留当前的上下文信息,调用内核中的缺页中断处理子程序(这里上下文切换其实并不算是进程切换,进程仍然是这个进程)。
子程序会先找到缺页的地址,然后判断内存中是否已经“满员”。如果不是那么子程序就直接把这段缺失的信息传进去。如果内存已经满了,那么系统会找到相应的内存段换下来(设计很复杂的算法),如果这段信息被修改过那么还会被写到外存。
7.9动态存储分配管理
动态存储分配相关的函数主要有malloc、calloc、realloc、free。
Malloc函数会向内存申请一个用户所需要的大小的内存,返回一个void *类型的指针。Free就是将获取的动态存储释放,还给系统。
由于动态存储分配是直接对内存进行操作,不当的操作很容易导致程序运行缓慢、资源浪费等问题。
所以要及时回收向系统要来的内存。另外释放内存后,一定要将相应的指针释放,设置为NULL,否则继续对这个指针进行操作,会让这片不知道用来干什么的内存发生以外的修改或调用,导致不可估计的错误。
7.10本章小结
对于程序的内存管理是个很复杂的课题。
系统为程序申请虚拟内存,建立映射。缺页中断时,向内存写入信息。
向内存中寻找信息也是个不简单的事情,逻辑地址、线性地址、物理地址,一步步转换才能真正找到相应的数据。不命中还需要一系列的操作。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备分为三种:字符设备、块设备、网络设备。Linux对设备的管理是将设备看作一种特殊的文件,然后通过linux的虚拟文件系统VFS来管理和控制各种设备。
8.2 简述Unix IO接口及其函数
Unix IO函数很简单,open(打开)、close(关闭)、read(读)、write(写)、lseek(定位)
8.2.1 open
8.2.2 close
8.2.3 read
(buf指的是存储器指针)
8.2.4 write
8.2.5 lseek
8.3 printf的实现分析
首先,我们要知道,printf函数是一种特殊的函数,它并没有固定数量的参数。
上图可见,在fmt后面有三个…表示不确定数量的参数(一般来说,不确定参数的函数至少要有一个参数)。这些不确定数量的参数就是格式串中需要使用变量代替的那些变量(printf(“%d”, x)比如说前面的x)。由于不确定函数的数量,后面的参数固然没有对应的名称,想要使用这些参数时,只能通过取地址(将fmt的地址,加上一定的值即可)。
已知上面的的内容,我们就来浅析printf的实现。
下面为printf函数的函数体
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
Arg存储的就是第一个变量的地址。
Vsprintf实现的是将格式串完全转变为字符串,将那些需要替换的变量加入其中。返回的i是需要打印的字符串的长度。
Write函数就是将所获得的字符串在屏幕上显示出来。Write的实现就比较复杂,包括保护进程上下文信息、调用syscall等(实力不足,不予说明)。
8.4 getchar的实现分析
键盘输入在IO中的优先级很高,每当键盘输入消息,CPU都会接受到一个中断信号。根据不同的中断向量,CPU会定位到不同的中断处理程序,保留寄存器信息,并根据输入信息不同采用不同的处理方式,还原现场即可。
Getchar函数实际上是调用了read函数,从缓存区读取一个字符。而缓存区可不一定只接受一个字符。调用getchar后缓存区会一直存储进键盘输入的信息,直到遇到回车或满了。
8.5本章小结
系统IO是对于设备的管理。通过将设备看作一种文件,来实现接受设备信息、向设备传输信息等操作。
一系列函数的实现更是离不开设备的参与。IO管理是操作系统的重要板块。
结论
Hello从被编写,到执行,到执行结束。对于程序员来说,可能只是一小段无关紧要的时间,但是在机器内部经历了浩大的一项工程。
从一行行指令到可执行文件,hello就经历了不少过程。预处理、编译、汇编、链接一项不可缺少。这些流程逐渐将高级语言转化为机器语言,供机器执行。
为了执行hello,系统会为调用fork、execve函数为hello创建一个新的进程。让hello在虚拟内存中有一块自己的一席之地。另外系统还会为hello构建从磁盘到虚拟内存、从虚拟内存到物理内存的映射。方便在执行时,系统从虚拟内存中获取想要的信息。为了实现系统的各项信息,IO管理也是不可或缺,依靠它系统从键盘等设备中获取用户输入的信息。
参考文献
2. 程序详细编译过程(预处理、编译、汇编、链接) - 知乎 (zhihu.com)
4. PLT & GOT 表动态链接详解及 pwn 应用_.got.plt 的序号-CSDN博客
5. 基础:正确理解CPU上下文切换_context switches per second-CSDN博客