HIT 2021春 CSAPP 大作业 程序人生 博客

摘  要

hello是程序员入门写的第一个程序,在对计算机系统并不了解的情况下,一个程序就此编译运行,其背后发生了难以想象的复杂变换,如今我们重新审视hello,探究其运行背后详细的规则和机制。可以更进一步了解我们的计算机系统。

本文将hello程序人生分为两大板块,一个是hello.c是怎么通过预处理、编译、汇编、链接形成可执行程序hello,里面所包含的许多概念、功能、指令、规则、机制、特点。另一个是hello作为可执行程序运行的过程中,被计算机系统作为进程管理、分配存储空间、通过系统IO与计算机硬件进行交互的许多知识点、规则、机制、和拓展。

我们探究的方法,便是将一个简短但覆盖全面的hello.c,通过他编译和运行的实例,通过不同工具对他产生的中间产物的解析和分析,来获得或者证明相关知识和规律。

在这个过程中,会收获许多意想不到的知识和经验总结。总而言之,这既是hello程序的人生,也是一个程序员对计算机系统的学习逐渐深入的人生。

关键词:预处理、编译、汇编、链接、进程、shell、存储、地址、cache、IO管理

目  录

第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 -

结论............................................................................. - 14 -

附件............................................................................. - 15 -

参考文献..................................................................... - 16 -

第1章 概述

1.1 Hello简介

通过Hello程序的自白,我们可以获知如下的流程

P2P(From Program to Process):

首先,Hello在文本编辑器中,或在命令行窗口使用vim,又是在IDE中,由程序员进行编写应用程序代码所得到的。在C语言中,得到的文件为hello.c。也就是P2P中的第一个P(Program)。

把Program,即hello.c在IDE已经配置好的环境中编译,亦或是在命令行中利用gcc -m64 -no-pie -fno-PIC hello.c -o hello进行编译,在编译的过程中,通过编译器驱动程序调动预处理器(cpp)、编译器(cl)、汇编器(as)、链接器(ld)四个程序获得二进制的可执行文件hello。

O2O(From Zero-0 to Zero-0):

当运行可执行文件hello的时候,加载器execve()将程序的_start函数的地址设置为程序的入口点,而_start函数调用了系统的启动函数__lib_start_main,然后进一步调用main函数,并处理返回值,这个过程是一个从无到有的过程,所以是From Zero。

而程序在运行的过程中,通过段式管理、页式管理,各级存储器联动,CPU分配时间周期,执行逻辑控制流,每条指令在流水线上取值、译码、执行、访存、写回。与此同时,内存管理单元和CPU处理器在执行过程中通过高速缓存、TLB、多级页表在物理内存中存取数据、指令,通过I/O系统输入输出。最后当程序运行结束时,进程被回收,内存被释放,有关的上下文被删除,几乎不留下什么,便是To Zero。

1.2 环境与工具

硬件:

X64 CPU;Intel Core i7; 16G RAM;

软件:

Windows10; VMware; Ubuntu 18.04; elfreader

开发与调试工具:

GCC; Objdump; Code:Blocks

1.3 中间结果

hello.c          源程序

hello.i           预处理后的源程序

hello.s          编译后的汇编语言

hello.o          汇编后的可重定位二进制程序

hello             链接后的可执行目标文件

hello1.txt      hello.o的反汇编文本

hello2.txt      hello的反汇编文本  

1.4 本章小结

本章是整个实验的开篇,简单叙述了hello程序的一生,即其开发的周期和运行的周期,分为P2P和O2O两个过程。同时介绍完成本实验学生的硬软件环境、开发和调试工具以及相关文件。

第2章 预处理

2.1 预处理的概念与作用

概念:

程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位--(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。

作用:

预处理程序(cpp)会识别以#开头的内容,在C语言中,如#include、#define等

若是#include,在预处理的过程中,则会读取后面的.h文件的内容,并将它们添加在程序源码中。若是#define,在预处理的过程中,则会替换所有前面的内容为后面的内容。除此以外还有很多预处理功能,他们都有一定的作用。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

Main函数没有发生变化,带#的include语句被替换成了成段成段的代码。

2.4 本章小结

本章节对hello.c文件进行了第一步的预处理,介绍了预处理的概念和功能,以及与它有关的许多应用和方法,在具体的实验操作上,对hello.c的处理让我们看到了对于头文件的解析,对于宏定义的替换。

第3章 编译

3.1 编译的概念与作用

概念:

简单讲,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。对于C语言来说,就是把上一步获得的合并后的源码转换为汇编语言。

作用:

编译器完成的功能是把源码(SourceCode)编译成通用中间语言(MSIL/CIL)的字节码(ByteCode)。编译后生成的.s汇编语言程序文本文件比预处理文件更容易让机器理解,但比.o可重定位目标文件更容易让程序员理解,是一个重要的中间过程。

3.2 在Ubuntu下编译的命令

gcc -S hello.c -o hello.s

3.3 Hello的编译结果解析

3.3.1 常量

常量存储在.rodata段,在此例子中为printf中的文本内容,被定义为.LC0,其中汉字被编码为三个字节

3.3.2 变量

在这里sleepsec是一个初始化了的全局变量,因此保存在.data段,但因为是int型变量,被存储为2,按4字节对齐,大小为4字节。

函数参数argc与3进行比较,从汇编语言中可以看出,它先存放在寄存器EDI中,在比较的时候放在栈中RBP-20的位置。

3.3.3 赋值

在汇编语言中赋值较多为mov一个立即数给寄存器。

如果将运算也视为一种赋值,那么汇编语言也可以通过这种形式让寄存器的数+1。

3.3.4 分支、循环、条件跳转

i<10被编译成了<=9,把cmpl比较后的结果作为jle是否执行的跳转。与此同理的还有上文的argc!=3。

跳转后执行的内容会不相同,这样的分支是通过jne、jle这类指令实现的

循环则是通过条件跳转的不断执行实现的。

3.3.5 数组

argv[1]和argv[2]的内容都是通过栈相对寻址得到的。argv[0]地址从栈使用movl语句传递给rax,使用addq+8,addq+16,计算argv[1],argv[2]地址,对其引用。

3.3.6 调用函数

汇编中函数的调用是通过call实现的,而参数的传递则是通过rdi、rsi、rdx、rcx实现的。

3.4 本章小结

本节内容对应书本第三章,从这里开始,理解难度就大幅上升了,本节介绍了将.i文件转化为.s文件的过程,而.s文件就是我们熟悉的汇编语言,在这个过程中,许多的内容可以产生对应的关系,但是汇编语言有自己的特殊规则,符合机器的处理逻辑,不符合人们的阅读习惯,而熟练掌握了这些内容是这里的重中之重,更容易理解高级语言的底层表示方法。

第4章 汇编

4.1 汇编的概念与作用

概念:

  汇编器(as)将hello.s文件翻译成二进制机器语言的指令,然后把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存到目标文件hello中。

作用:

汇编过程将汇编代码转换为计算机能够理解并执行的二进制机器代码。

4.2 在Ubuntu下汇编的命令

gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

4.3 可重定位目标elf格式

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

readelf hello.o -e

可以读出是小端序、UNIX内核、x86-64等信息。

节头部表描述了这些节的相关信息

4.4 Hello.o的结果解析

objdump -d -r hello.o > hello1.txt

此版本与第三节的汇编语言版本hello.s大同小异

差异如下:

  1. 此版本立即数为十六进制,而hello.s中为十进制
  2. 跳转的时候此版本直接跳入某个语句,而hello.s中有L0 L1

3.调用函数的时候此版本直接进行重定向,而hello.s中只是声明函数名称

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

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

5.3 可执行目标文件hello的格式

同上

需要额外留意的是动态链接库,里面记录了许多偏移量和信息。

5.4 hello的虚拟地址空间

使用edb加载hello:

虚拟空间从0x400000开始。结合上面ELF表中的内容获知:

.interp段地址从0x4001c8,偏移量为0x1c8,大小为0x1c

5.5 链接的重定位过程分析

objdump -d -r hello > hello2.txt

同样地,hello中间部分与hello.o大致相同,差异如下:

  1. 此版本反汇编代码从0x400000开始,是实地址,而hello.o是从虚拟地址0开始的。
  2. hello因为在重定位的过程中加入了大量函数、变量,main在稍后的位置,而hello.o的main函数前面仅有.text段
  3. 调用函数,条件跳转时,该版本的地址都为绝对或相对的地址,而非hello.o中的虚拟地址。

5.6 hello的执行流程

名称                                                              地址

ld-2.23.so!_dl_start                                       0x00007f8dec5b79b0

ld-2.27.so! dl_init                                         0x00007f8dec5c6740

hello!_start                                                    0x004004d0

ld-2.27.so!_libc_start_main                          0x00400480

libc-2.27.so! cxa_atexit                                0x00007f8dec226280

hello!_libc_csu_init                                      0x00400580

hello!_init                                                     0x00400430

libc-2.27.so!_setjmp                                     0x00007f8dec221250

libc-2.27.so!_sigsetjmp                                0x00007f8dec221240

libc-2.27.so!__sigjmp_save                          0x00007fa8dec221210

hello_main                                                    0x004004fa

hello!puts@plt                                              0x00400460

hello!exit@plt                                               0x004004a0

hello!printf@plt                                            0x00400470

hello!sleep@plt                                             0x004004b0

hello!getchar@plt                                         0x00400490

ld-2.23.so!_dl_runtime_resolve_avx            0x00007f8dec5cd870

libc-2.27.so!exit                                            0c00007f6002de35b0

5.7 Hello的动态链接分析

动态链接项目中,查看dl_init前后项目变化:

在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令。

5.8 本章小结

本章将hello.o以及其他内容连接成了hello可执行程序,并将其与之前的反汇编文件进行了差不多的解读,然后进行了对比,最后深入了解了动态链接项目。

6章 hello进程管理

6.1 进程的概念与作用

概念:

进程的定义是一个正在执行的程序实例,每个程序都在某个进程的上下文中运行,上下文是由程序正确运行所需的状态组成的。包括代码、数据、堆栈、寄存器、程序计数器、环境变量和打开文件描述符。

作用:

       进程机制可以产生一种程序是当前唯一程序的假象,这样可以让多个进程“独占”内存和处理器。事实上所有的程序都在并发运行。

6.2 简述壳Shell-bash的作用与处理流程

Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。

它交互式解释和执行用户输入的命令或者自动地解释和执行预先设定好的一连串的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。

Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。

Bash还能从文件中读取命令,这样的文件称为脚本。和其他Unix shell 一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。

在Shell实验中我们曾经认识过处理流程:

处理流程:第一步:用户输入命令。第二步:shell对用户输入命令进行解析,判断是否为内置命令。第三步:若为内置命令,调用内置命令处理函数,否则调用execve函数创建一个子进程进行运行。第四步:判断是否为前台运行程序,如果是,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令。第五步:shell应该接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程

一个程序作为进程在运行时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序

当加载器运行时,它会创建一个内存映像。在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,接下来,加载器跳转到程序的入口。

6.5 Hello的进程执行

多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。

  操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重启一个被抢占的进程所需得状态。

  在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个决策称为调度。

  hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。所以其实hello在sleep时就是这样的切换。

  程序在进行一些操作时会发生内核与用户状态的不断转换。这是为了保持在适当的时候有足够的权限和不容易出现安全问题。

6.6 hello的异常与信号处理

不停乱按,包括回车:

对程序的输出无影响,且输入的字符串会留在缓冲区中在程序结束后尝试运行

Ctrl+C:

直接终止了进程

Ctrl+Z:

进程停止

然后输入ps:

然后输入jobs:

然后输入pstree:

然后输入fg:

发现程序继续运行,再次Ctrl+Z停止

输入kill:

发现进程被杀死。

6.7本章小结

本章主要介绍了进程的概念和作用,首先是shell构建起了用户和系统内核之间的交互。然后描述了进程在shell中不同的情况,如fork创建子进程,用execve执行程序,还有各种上下文切换,进程控制。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:

在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。

物理地址:

在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。

虚拟地址:

CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。

线性地址:

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

逻辑地址由两个部分组成,一个是段标识符,一个是段内偏移量,段标识符中前13位是索引号,后面三位包含硬件的细节。

       许多段的描述符组成了段描述符表,可以通过段描述符的前13位来找到具体的描述符,然后通过这种方式来查找Base字段。它描述了一个段的开始位置的线性地址。

首先,给定一个完整的逻辑地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

把Base + offset,就是要转换的线性地址了。

7.3 Hello的线性地址到物理地址的变换-页式管理

计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。因为所有的地址翻译都是在芯片上的MMU中进行的,因此非常快。

多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到最后确定对应的物理页号,与VPO结合。如果是二级页表,第一级页表的每个PTE负责一个4MB的块,每个块由1024个连续的页面组成。二级页表每一个PTE负责一个4KB的虚拟地址页面。这样的好处在于,如果一级页表中有一个PTE是空,那么二级页表就不会存在,这样会有巨大的潜在节约,因为4GB的地址空间大部分都是未分配的。现在的64位计算机采用4级页表,36位的VPN被封为4个9位的片,每个片被用作一个页面的偏移,CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1PET的偏移量,这个PTE包含L2页表的基地址,VPN2提供一个到L2PTE的偏移量,以此类推。

7.5 三级Cache支持下的物理内存访问

对于一个虚拟地址请求,首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就先在结合多级页表,得到物理地址,去cache中找,到了L1里面以后,寻找物理地址又要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3。这里就是使用到CPU的高速缓存机制了,一级一级往下找,直到找到对应的内容。

7.6 hello进程fork时的内存映射

当一个进程fork时,内核为hello进程创建各种数据结构,分配给他了唯一的PID,为了给新的hello进程创建虚拟内存,需要创建新进程的mm_struct、区域结构和页表的原样副本。两个进程的页面都标记为只读,并标记为写时复制,因此fork的返回时两个进程的虚拟内存是相同的,而任何一个进程进行写操作时,就会复制新的页面。

7.7 hello进程execve时的内存映射

删除已存在的用户区域。删除当前进程(shell)虚拟地址的用户部分中的已存在的区域结构。 映射私有区域。为hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。 设置程序计数器(PC) 。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

7.8 缺页故障与缺页中断处理

虚拟内存中,DRAM缓存不命中称为缺页。CPU需要引用某个VP中的一个字,然后读取PTE时,发现有效位为0,则说明需要的字不在内存里,这时就发生了缺页异常。缺页异常发生时,通常会调用内核里的缺页异常处理程序,该程序会选择一个牺牲页。当异常处理程序返回时,它会重新启动导致缺页的指令。

牺牲页的具体机制如下:选择一个VP,如果该VP已经被修改,内核就会将它写回磁盘。无论哪种情况,内核都会修改这个VP的页表条目,反映出这个VP已经不再缓存在内存里面。接下来,内核从磁盘复制缺页的VP到内存中的刚刚的PP中,更新PTE,然后返回。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区 域,称为堆(heap)系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。

分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。

隐式空闲链表:

带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。

显式空间链表:

显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。

7.10本章小结

本章又是书本中的一个重难点,也就是有关内存管理的知识,例如从逻辑地址到线性地址再到物理地址,还有虚拟内存和物理内存的关系,段式管理、页式管理的方法,还有缺页故障和缺页中断管理机制,如何根据缓存或者页表寻找物理内存。还有在这个过程中程序怎么优化和加速内存的读写。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备在hello中都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O接口。

8.2 简述Unix IO接口及其函数

接口:

打开文件: 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

改变当前的文件位置: 对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的而文件,当k>=m时执行读操作会触发一个成为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

关闭文件: 当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

函数:

Open函数:进程是通过调用open函数来打开一个存在的文件或者创建一个新文件的,函数声明如下:int open(char *filename, int flags, mode_t mode);open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置成mode&~umask。

Read和Write函数:应用程序是通过分别调用read和write函数来执行输入和输出的。函数声明如下:ssize_t read(int fd, void *buf, size_t n);ssize_t write(int fd, const void *buf, size_t n);read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

Close函数:进程通过调用close函数关闭一个打开的文件。函数声明如下:int close(int fd);成功返回0错误返回EOF。

8.3 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;

}

第一句va_list arg = (va_list)((char*)(&fmt) + 4);这句的作用是表示token中的第一个参数。

而第二句i = vsprintf(buf, fmt, arg);返回了一个长度,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

而第三句write(buf, i);write函数将buf中的i个元素写到终端。

陷阱-系统调用 int 0x80或syscall.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

int getchar(void)

{

       static char buf[BUFSIZ];//缓冲区

       static char* bb=buf;//指向缓冲区的第一个位置的指针

       static int n=0;//静态变量记录个数

       if(n==0)

       {

              n=read(0,buf,BUFSIZ);

              bb=buf;//并且指向它

       }

       return(--n>=0)?(unsigned char)*bb++:EOF;

}

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

       当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了Linux怎么实现IO设备的管理方法,怎么将显示器、键盘视为文件进行读写操作,分析了UnixIO的接口和函数,分析了printf和getchar函数。

结论

Hello的历程:

首先由程序员从无到有地写下C语言源程序hello.c。

然后进行预处理,将带#的指令进行解析替换,生成了hello.i文件。

然后进行编译,对源代码进行编译,得到汇编语言,生成了hello.s文件。

然后进行汇编,对汇编语言转换成机器代码,生成重定位信息,生成了hello.o文件。

然后进行链接,与动态库链接,生成可执行文件,生成了hello。

然后创建进程,在shell,也就是我们的Linux控制台中运行hello程序,父进程通过fork函数为hello创建了进程。

然后加载程序,通过加载器调用execve函数,删除原来进程的内容,加载当前我们进程的代码,等进程进入自己上下文中的虚拟内存空间。在这个过程中,内存管理单元MMU、饭以后呗缓冲期TLB、多级页表机制、三级cache同舟共济,共同高效而准确地完成程序对地址的请求。同时,异常处理机制保证了hello对异常信号的处理,使之正常平稳运行,UnixI/O让hello可以正确地获得键盘的输入,打印对显示器的输出。

最后当程序运行完成之后,shell父进程回收hello作为子进程的资源,内核删除了为这个进程创建的所有数据结构,好似它从来没有来过一样。

总结:

在hello程序的整个实现过程中,经历了许多个阶段,在操作系统、硬件软件的参与和互相配合下,完成了一个个复杂机制的处理,达成了一个又一个简单的任务。而这一个又一个简单任务的正确完成,又共同构成了hello程序完美而正确的运行,在这整个过程中,或许有过于复杂的机制或看似多余的流程,但是这是多年以来无数智慧结晶不断优化和发展的计算机对于程序的处理流程。这些复杂的机制高效而正确地完成着自己的使命。

看着hello的程序人生一路走来,竟有一丝感动,但更多的是对于知识和智慧的感叹。

或许,未来有更简单更高效的程序人生,但在当下,只有好好了解了这些内容,才能争取到对未来更美好的可能性。谢谢你,hello。

  

附件

hello.c          源程序

hello.i           预处理后的源程序

hello.s          编译后的汇编语言

hello.o          汇编后的可重定位二进制程序

hello             链接后的可执行目标文件

hello1.txt      hello.o的反汇编文本

hello2.txt      hello的反汇编文本 

参考文献

[1]  兰德尔E布莱恩特. 《深入理解计算机系统》. 机械工业出版社,2017:7-1.

[2]  Pianistx. printf 函数实现的深入剖析.

博客园:https://www.cnblogs.com/pianist/p/3315801.html

[3]  博韦. 深入学习linux内核 第二章.

CSDN:https://blog.csdn.net/weixin_33946020/article/details/89865801

[4]  wxzking. Linux下逻辑地址-线性地址-物理地址图解.

CSDN:https://blog.csdn.net/wxzking/article/details/5905214

[5]  百度百科. 编译器.

百度百科:https://baike.baidu.com/item/%E7%BC%96%E8%AF%91%E5%99%A8

[6]  绘夜. C语言的预处理详解.

CSDN:https://blog.csdn.net/czc1997/article/details/81079498

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值