摘 要
文章主要写了Hello的一生,即Hello.c这个源程序所经历的预处理、编译、汇编、链接这一系列过程。同时还包括了它的进程管理、存储管理、IO管理。通过对上述内容观察与描述,来加深对程序的编译、加载、运行的了解
关键词:Hello;预处理;编译;汇编;链接;进程管理;存储管理;IO管理;
第1章 概述
1.1 Hello简介
首先,hello.c是由程序员通过IDE或者文本编辑器编写的程序代码文件,而后通过编译过程,分别获得hello.i,hello.s,hello.o,最后获得hello可执行文件。
图1.1 编译系统
1.2 环境与工具
硬件环境:
X64 CPU;Intel Core i7 8750H; 2.20GHz; 16G RAM;
软件环境:
Windows10,VMware16 Pro,Ubuntu 20.04LTS
开发与调试工具:
GCC,GDB,Objdump,Hexedit,Clion
1.3 中间结果
编译过程文件:
hello.c:源程序文件->hello.i:预处理后的文本文件->hello.s:编译后汇编程序文本文件->hello.o:汇编后的可重定位目标程序(二进制文件)->hello:链接后的可执行目标文件
其他文件:
hello_o.txt:hello.o的反汇编文件
hello_dis.txt:hello的反汇编文件
hello_1elf.txt:ELF格式下的hello.o
hello_2elf.txt:ELF格式下的hello
1.4 本章小结
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。同时介绍实验相关的软硬件环境与开发工具。
第2章 预处理
2.1 预处理的概念与作用
概念:
预处理,在程序设计领域,一般是指程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理为特定的单位。
最常见的预处理是C语言和C++语言。ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase),通常前几个阶段由预处理器实现。
在学习使用当中,较为常见的#include,#define就属于预处理指令。除此之外,还有一些其它的指令:#if,#ifdef,#ifndef,#else,#elif,#endif(条件编译),#line(行控制),#error(错误指令),#pragma(和实现相关的杂注),单独的#(空指令)。
作用:
例如C语言中的预处理指令#include <stdio.h>,会告诉预处理器读取系统头文件stdio.h的内容,从而引入该库中的内容。预处理器还会替换程序起始位置的宏。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
使用命令gcc -E hello.c -o hello.i
图2.1获得.i文件
产生hello.i文件,打开之后可以看到如下文本内容。
图2.2.i文件的部分内容
2.3 Hello的预处理结果解析
查看hello.i的内容发现,主函数之上的头文件全部都被展开了,其中包括了对于各种预处理指令的解析,各种#include的引用。
图2.3函数主体部分
同时,我们还能看见许多结构的定义,除此之外还有对外部变量的引用和对引用目录的标注等等。
图2.4其他部分的展开
2.4 本章小结
第二章的内容是与预处理阶段相关的知识,其中包括了对于预处理指令概念和作用的理解,并通过在Ubuntu下执行预处理命令协助理解。同时对hello的预处理结果解析,对预处理指令和stdio.h等头文件有了更深刻的理解。
第3章 编译
3.1 编译的概念与作用
概念:
编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。它把预处理文件进行词法分析、语法分析、语义分析、优化,从C语言等高级语言转换为成机器更好理解的汇编语言程序,转换后的文件仍为ASCII文本文件。
作用:
.s文件相比.i文件来说,更容易让机器去理解,相比之后的.o文件有更容易让人理解。是非常重要的一步存在。
3.2 在Ubuntu下编译的命令
使用命令gcc -S hello.c -o hello.s
图3.1获得.s文件
产生的.s文件内容如下:
3.3 Hello的编译结果解析
图3.2.c文件的内容
3.3.1 数据
(1)常量:
查看.c文件内容,可知printf函数中的字符串是常量,在.s文件中分别是.LC0,.LC1后面的。
图3.3常量
- 变量:i、argc、argv为局部变量。
图3.4局部变量1
argv存在寄存器edi中与4做比较,而再往下,argc存在了寄存器ris中;
图3.5局部变量2
i如果小于等于7,就进入循环。
3.3.2 赋值
程序中,存在对计数器变量i的赋值,开始赋初值0,然后跳到L3判断;
图3.6赋值1
除此之外,还有每次循环结束后,i加一;
图3.7赋值2
3.3.3 类型转换
在for循环体内部,使用了atoi函数将字符串转换为int类型。
图3.8类型转换
3.3.4 算术操作
在赋值阶段已经出现过,即每次循环结束后都要进行一次i++操作;
图3.9算术操作
3.3.5 逻辑操作
程序源码中没有出现逻辑操作。
3.3.6 关系操作
图3.10关系操作1
图3.11关系操作2
3.3.7 数组操作
数组操作涉及到了对argv数组的引用;
图3.12数组操作
3.3.8 函数操作
3.3.9 控制转移
(1)if判断:
存在寄存器edi中的argc和4进行比较,若相等则直接跳转到L2;否则执行if里的语句。
图3.13if循环
(2)for循环:
L2,L3分别负责i赋初值和进入循环判断的任务,而L4则是循环体内部。
图3.14for循环
3.4 本章小结
本章主要了解了编译器将.i文件转换为.s汇编文件的过程,同时以C语言为例,对于文件内部的各种数据以及操作进行了详细的叙述,加深了对于高级语言和底层执行之间关系的理解。
第4章 汇编
4.1 汇编的概念与作用
概念:
汇编器将hello.s文件翻译成二进制机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存到目标文件hello.o中。hello.o是一个二进制文件,包含着程序的指令编码,如果用文本编辑器查看,将看到一堆乱码。
作用:
把汇编文件转换为机器容易理解的机器代码(二进制文件),该文件的内容是程序在该机器的机器语言的表示。
4.2 在Ubuntu下汇编的命令
使用命令gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o
图4.1获得.o文件
图4.2得到hello.o文件
4.3 可重定位目标elf格式
使用命令readelf -a hello.o > hello1elf.txt
图4.3elf格式查看
4.3.1ELF头
查看ELF头,其中含有的信息有系统的字的大小和字节顺序,ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图4.4ELF头信息
例如我的ELF开头为:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00图中还能看出,小端序排列,大小为64字节,文件是重定位的类型,与我们的命令一致,机器为X86-64,节头部表的文件偏移为0,节头数量为14,字符串表索引节头为13。
4.3.2节头
可以看出,节头描述了所有节的基本信息,而他们本身的意义如下:
.text节:已编译程序的机器代码
.rela.text节:一个.text节中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.data节:已初始化的静态和全局C变量。类型为PROGBITS,意为程序数据,旗标为WA,即权限为可分配可写。
.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。类型为NOBITS,意为暂时没有存储空间,旗标为WA,即权限为可分配可写。
.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。类型为PROGBITS,意为程序数据,旗标为A,即权限为可分配。
.comment节:包含版本控制信息。
.note.GNU_stack节:用来标记executable stack(可执行堆栈)。
.eh_frame节:处理异常。
.rela.eh_frame节:.eh_frame的重定位信息。
.shstrtab节:该区域包含节区名称。
.symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
图4.5节头信息
4.3.3符号表
可以看到一共提供了18个符号,其中:
main是一个位于.text节(Ndx=1)偏移量(value)为0,大小为146个字节的全局符号,类型为变量。而下方的puts、exit、printf、sleep、getchar等函数为NOTYPE未知类型,未定义(UND)符号。hello.c为文件,ABS表示不该被重定位的符号。
图4.6符号表
4.3.4.rela.text节和.rela.eh_frame节
图中信息介绍了两条重定位节的一些信息,这里同时涉及到了重定位的绝对引用与相对引用,还需要了解绝对引用、相对引用的重定位算法。
绝对引用重定位算法:
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr);
相对引用重定位算法:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
图4.7两条重定位节
4.4 Hello.o的结果解析
执行命令objdump -d -r hello.o > hello_o.txt
图4.8获得反汇编文件
与.s文件相比,最明显的不同便是.o文件有代码前方有机器码,而.s没有。
跳转上,.o文件是依照地址跳转,而.s文件是依照标识符跳转。
申请栈时,.o文件是16进制写法,.s文件为十进制。
重定位时,.o文件会留下地址,而.s文件则直接声明。
图4.9反汇编文件内容
4.5 本章小结
本章主要了解了.s文件到.o文件的转换,查看了程序的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
图5.1获得hello文件
5.3 可执行目标文件hello的格式
使用命令readelf -a hello > hello2elf.txt
图5.2获得ELF格式的文件
首先是熟悉的ELF头,一部分内容与之前的相同,而文件类型成了可执行文件,一些节头部相关信息也发生了变化,内容如图:
图5.3ELF头
其次是节头部分,由图可见,相比1elf中内容多了许多。
图5.4节头
接下来是程序头部分:
图5.5程序头
符号表也有两部分,内容如下:
图5.6符号表
Section to Segment mapping和Dynamic section,内容如图:
图5.7Section to Segment mapping
图5.8Dynamic section
之后是重定位节内容,有如下两部分:
图5.9重定位节
最后是一些其他信息:
图5.10其他信息
5.4 hello的虚拟地址空间
使用edb加载hello:
图5.11edb加载hello
由图可知,程序的起始虚拟地址位于0x400000。
而.interp段地址从0x4002e0,偏移量为0x2e0,大小为0x1c(别忘了最后的点),对齐要求为1。
图5.12.interp段
.text段地址从0x4010f0,偏移量为0x10f0,大小为0x140,对齐要求为16。
图5.13.text段
.rodata段地址从0x402000开始,偏移量为0x2000,大小为0x3b,对齐要求为8。
图5.14.rodata段
5.5 链接的重定位过程分析
使用命令objdump -d -r hello > hello_dis.txt
图5.15将反汇编文件重定位到helllo_dis.txt中
最明显的不同在于,hello的反汇编代码地址是0x40xxxx的类型,是经过重定位的,而.o文件则是虚拟地址0开始。这一点同样体现在两种反汇编文件的函数调用的地址显示上。
除此之外,.o文件中先是.text段,然后是main函数。而hello的反汇编中则存在各种其他的复杂函数与数据。
图5.16hello的部分反汇编文件
其中使用到的最基本的两种重定位类型为:重定位PC相对引用,重定位绝对引用。正是通过这两个基本的重定位类型,完成重定位符号引用,我们所看到的指令后面都有确定的地址偏移。
5.6 hello的执行流程
子程序名 | 子程序地址 |
ld-2.23.so!_dl_start | 0x7fdc27adbdf0 |
ld-2.27.so! dl_init | 0x7fdc27aebc10 |
ld-2.27.so!_libc_start_main | 0x401145 |
libc-2.27.so! cxa_atexit | 0x7fdc27aef550 |
libc-2.27.so!_setjmp | 0x7fdc27ae60d0 |
libc-2.27.so!_sigsetjmp | 0x7fdc27ae4460 |
libc-2.27.so!__sigjmp_save | 0x7fdc27adb396 |
hello!puts@plt | 0x401090 |
hello!exit@plt | 0x4010d0 |
hello!printf@plt | 0x4010a0 |
hello!sleep@plt | 0x4010e0 |
hello!getchar@plt | 0x4010b0 |
0x7fdc27aec980 | |
libc-2.27.so!exit | 0x7fdc27adb2f0 |
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表(PLT)+全局偏移量表(GOT)实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
图5.17节头中.got的相关信息
下面使用edb查看相关位置:
图5.18.got
图5.19.got.plt
在之后的调用函数时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。
5.8 本章小结
本章介绍了链接的相关内容,熟悉了链接的指令,如何将.o文件转为可执行文件。回忆了edb的使用方法,分析了重定位的相关知识。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
作用:
通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:
作为命令处理器,接受用户输入的命令,然后根据命令进行相关操作,比如调用相关的程序。作为命令语言,它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令。作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。
处理流程:
- 从终端读入用户输入的命令
- 对命令解析,并判断其是否为内置命令
- 若是内置命令,则直接执行
- 若不是,则调用execve函数创建子进程运行
- 再判断是否为前台运行程序,若是,则调用等待函数,等待前台程序结束。否则程序转入后台,接受用户下一步输入的命令
- Shell接受键盘输入的信号,并且应该对信号产生相应的反应
- 回收僵死进程
6.3 Hello的fork进程创建过程
当输入一个非内置shell命令时,比如./hello,shell会将hello识别为可执行程序,因此会调用某个驻留在存储器中被称为加载器的操作系统代码来运行它。
具体是父进程fork一个子进程,这个子进程就是目标程序,而子进程与父进程非常相似,但PID不同。
图6.1子进程与父进程
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。
图6.2execve函数
execve函数加载并运行可执行文件filename(hello),且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。
当加载器运行时,它创建一个内存映像。在程序头部表的引导下,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的堆和栈会被初始化为0,通过将虚拟地址空间的页映射到可执行文件的页大小的片,加载器从可执行目标文件中读入.init /.text/.rodata/.data/.bss。然后开始执行。
图6.3进程虚拟内存
6.5 Hello的进程执行
多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。而上下文是内核重新启动一个被抢占的进程所需的状态,它右通用寄存器、浮点你寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
操作系统内核使用一种称为上下文切换的较高层次形式的异常控制流来实现多任务。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决策叫做调度,是由内核中被称为调度器的代码处理的。
而一个进程执行它的控制流的一部分的每一时间段叫做时间片。
图6.4进程上下文切换
处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核态(又是也叫超级用户态)。一个运行在内核态的进程可以执行指令集中的任何指令,并且可以访问系统的任何内存位置。
由图6.4可以看到,用户态和内核态直接是不断切换的,例如执行hello程序的时候,若在调用sleep函数之前,hello程序被抢占,就进行上下文切换,调用sleep时,进入内核态,处理器处理sleep并请求主动释放当前进程。之后计时器开始计时,内核进行上下文切换,执行其他的进程。当计时结束时,计时器发送一个中断信号,处理器处理中断信号,并进行上下文切换,重新回来执行hello进程。
6.6 hello的异常与信号处理
会出现四类异常:中断,陷阱,故障,终止。
如果乱按且结尾没有回车,这些东西会被当做字符串缓存,而如果结尾是回车,它之前的信息会被当作输入的命令。
图6.5不停乱按+回车and正常执行
发送一个SIGTSTP信号给前台进程组的每个进程,停止前台作业,即停止hello程序。
图6.6Ctrl+Z
让内核发送一个SIGINT信号给到前台进程组中的每个进程,终止前台进程,即终止hello程序。
图6.7Ctrl+C
查看进程信息,发现hello还在后台。
图6.8Ctrl+Z后ps
输入jobs后,看到进程停止。
图6.9Ctrl+Z后jobs
打印所有进程的关系。
图6.10Ctrl+Z后pstree
使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行,并且是继续刚才的进程,输出剩下的信息。
图6.11Ctrl+Z后fg
杀死进程。
图6.12Ctrl+Z后kill
6.7本章小结
本章主要介绍了进程的相关概念以及操作,理解了shell的概念与应用,同时fork,execve函数以及上下文切换也都非常重要。也是在这一章,hello的可执行程序得以产生,但运行途中还是会产生许多差错,这些差错给了我们学习的机会。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。
线性地址:
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。
虚拟地址:
CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
物理地址:
在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由两部分组成:段标识符,段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,剩下3位包含硬件信息。其中索引号可以直接理解成数组下标,它对应的“数组”就是段描述符表,段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
给定一个完整的逻辑地址[段选择符:段内偏移地址],
看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,基地址就知道了。
把基地址 + 偏移量,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
计算机会利用页表,通过MMU来完成从虚拟地址到物理地址的转换。而页表是一个页表条目(PTE)的数组,将虚拟页地址映射到物理页地址。
线性地址即虚拟地址,用VA来表示。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。
图7.1使用页表的地址翻译
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:翻译后备缓冲器,其中每一行都保存着一个由单个PTE组成的块。通过这种方式我们可以再把VPN分成TLBT(TLB标记)和TLB索引(TLBI),根据索引和标记在TLB中寻找对应的PPN,TLB命中可以减少内存访问,就和之前的cache命中类似,这里少了行,也可以理解成一组只有一行,类似直接映射。
图7.2虚拟地址中用于访问TLB的组成部分
多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。
图7.3一个两级页表的例子
多级页表中,VPN被分为k个部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到最后确定对应的物理页号,与VPO结合,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。
7.5 三级Cache支持下的物理内存访问
收到虚拟地址之后,先在TLB中寻找,若不在TLB中,结合多级页表得到它的物理地址,然后到Cache里面寻找。若在TLB中,则直接被MMU得到。
在Cache中,命中就返回,不命中就依次在L1、L2、L3中不断寻找。
图7.4TLB与Cache结合访问
7.6 hello进程fork时的内存映射
调用fork函数时,会创建一个基本与父进程相同的子进程,并为子进程分配一个独立的PID。在这个过程中,内核创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。同时,写时复制机制也会在其中一个进程写操作时创建新的页面,由此也就有了私有地址的概念。
图7.5一个共享对象
图7.6一个私有写时复制对象
7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。而加载程序的步骤如下:
(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构
(2)映射私有区域,为新程序的代码、数据、bss、和栈区域创建新的区域,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data、.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),execve做的最后一件事就是设置当前进程上下文的PC,使其指向代码区的入口。
图7.7加载器映射用户地址空间的区域
7.8 缺页故障与缺页中断处理
缺页:虚拟内存中的字不在物理内存中(BRAM缓存不命中),通俗地讲就是内存中没有这个页,所以导致MMU找不到对应的物理地址。缺页故障是一种故障,当指令引用一个虚拟地址,MMU会查找页表,当找不到对应的物理地址时,就会触发缺页中断。
图7.8触发缺页中断
当MMU触发缺页中断,缺页异常处理程序在内存中选择一个牺牲页,从其他存储器(磁盘)中读入这个页替换牺牲页,接着导致缺页的指令重新启动,页面命中。
图7.9触发缺页中断的处理
7.9动态存储分配管理
动态内存:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
基本方法与策略:
(1)带边界标签的隐式空闲链表分配器管理
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
图7.10隐式空闲链表
在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
图7.11隐式空闲链表的组织堆
(2)显式空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
图7.12双向空闲链表的堆块格式
7.10本章小结
本章主要了解了内存管理的的相关知识,包括了虚拟地址空间,如何映射,地址翻译等。同时在Intel环境下,段式管理和页式管理、fork和exceve的内存映射也同样重要,还知道了缺页故障和缺页中断管理机制,以及如何根据缓存或页表寻找物理內存。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而文件就是一个字节序列。所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
接口作用:
Unix IO接口能够将设备映射成文件,从而统一输入输出。
函数:
打开文件:应用程序通过open函数要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
关闭文件:当应用完成了对文件的访问之后,它就使用close函数通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。分别通过read、write操作执行。
改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行lseek,显式地将改变当前文件位置k。
8.3 printf的实现分析
图8.1printf函数
先看printf函数,它接收了一个fmt,然后将其送入vsprintf函数输出。
图8.2vsprintf函数
vsprintf函数接收fmt(字符串格式),它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
用户输入命令后,getchar从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章了解了Linux下IO设备的管理方法,IO接口及其函数,对于printf和getchar函数有了更深刻的理解(它们也不是最基础的函数)。
结论
从程序员敲下代码,形成hello.c的源程序开始,我们与程序的关系渐渐加深。虽然在以前看来,短短的小代码跑起来不过是几秒钟的事,但在深入了解了它的一生之后,我才发现一切并没有那么简单。
开始的代码,经过预处理,将各种需要的头文件展开、引入。处理完这些的.i文件有经过编译器的洗礼,化身成为汇编代码。在之后,.s中的汇编代码被转换成只有机器认识的机器代码,生成重定位信息,.o文件也在这时诞生。一个文件到这一步已经不容易了,可是这对于hello来说还没结束,它还要经历动态库链接,才能成为可执行文件。
下面就到了运行的阶段,它借用shell小跑起来,用fork来创建子进程,用execve帮它加载,在上下文间不断切换。与此同时,它随时有可能被中断,甚至承担着被杀死的风险。
在度过上述难关之后,它的运行又涉及到虚拟地址、内存引用、cache等等知识,越来越复杂,也越来越高效。同时异常处理机制保证了程序对异常信号的处理,使得程序之间都能在系统之下和平共处。
最后,hello完成了自己的实名,尘归尘,土归土。它终究逃不过被回收的命运,但或许是屏幕上的几行输出,又或许是某个文件中的数据,它总归是留下了些东西。
附件
hello.c:源程序文件
hello.i:预处理后的文本文件
hello.s:编译后汇编程序文本文件
hello.o:汇编后的可重定位目标程序(二进制文件)
hello:链接后的可执行目标文件
hello_o.txt:hello.o的反汇编文件
hello_dis.txt:hello的反汇编文件
hello_1elf.txt:ELF格式下的hello.o
hello_2elf.txt:ELF格式下的hello