哈尔滨工业大学CSAPP大作业

计算机科学与技术学院

2021年5月

摘  要

    本文将根据深入理解计算机系统这门课程所学以及书本内容,探究每个程序员最先接触的一个程序:hello world。本文将结合gcc,edb等工具,在linux系统之中研究这个程序的生命周期。

关键词:hello world;CSAPP;程序生命                           

目  录

第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过程:也就是从程序(program)变为进程(process)的过程。Hello程序从键盘输入诞生到一个C语言文件之中,经过我们通称为编译器的软件的一系列操作:预处理器、汇编器、编译器、链接器……的处理,他从一个文本文件变成了一个可执行文件。当我们在shell中键入运行该可执行文件的命令之后,shell生成子进程,这样,可执行文件就变成了一个实际上正在运行的进程。这就是Hello从程序变为进程的概述。

Hello程序的020过程:Hello进程创建之前不占用任何资源,创建之后也不占用任何资源。创建Hello进程之时是CPU为其分配内存、时间片,而Hello死亡之后(收到信号或是自然结束),shell自动回收其僵尸进程,其内部的数据结构也被删除。

1.2 环境与工具

硬件环境:

处理器:AMD Ryzen 5 5600H with Radeon Graphics      3.30 GHz

内存:16.0 GB (15.4 GB 可用)

硬盘:512GBSSD

软件环境:

      VMware Workstation 16:分配磁盘空间20GB 处理器1个6核 内存2GB

      Ubuntu 20.04

开发与调试工具:

      gcc,edb,CodeBlocks,objdump,readelf

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.i: .c文件经过预处理之后的文本文件。

hello.s: hello文件编译之后的汇编文件。

hello.o:  hello文件汇编之后形成的可重定位目标执行文件。

hello: 链接之后生成的可执行文件。

output.txt:  hello可执行文件的反汇编文件,查看其汇编代码。

output0.txt:  hello.o可重定位目标执行文件的反汇编文件。

hello.elf:hello.o文件的elf格式。

1.4 本章小结

简述了Hello程序的P2P和020过程、实验所用的软硬件环境、实验所生成的各个中间文件。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

所谓的预处理是指在进行正式编译(语法分析,代码生成,优化等)之前所做的工作。C语言的预处理包括了对引用库文件的预处理、对宏定义的预处理以及条件编译的预处理。

预处理的作用:

对原有的C程序进行预处理之后,原程序就不包含预处理的部分了。例如我们引用的<stdio.c>该库之中的内容直接加入到代码之中,我们使用宏定义#define定义的语句也直接被替换掉,还有根据#if决定是否处理之后的代码。

经过这样的处理,程序会变得更加方便阅读、修改、移植与调试,也有利于模块化的程序设计。

2.2在Ubuntu下预处理的命令

 

图2.1 Ubuntu下预处理命令

预处理命令:cpp hello.c>hello.i

2.3 Hello的预处理结果解析

 

图2.2 Hello的预处理结果


我们直接使用文本编辑器打开hello.i,发现我们所引用的库文件以及展开了几千行,我们在文件的最下端找到main函数,发现它基本没有改变。

 

 

图2.3 Hello预处理的库引用查找

当然我们知道,在我们#include的库文件中,还有无穷多个#include,所以,我们预处理的过程进行了如上图的搜索(当然,这仅仅是对stdio.h这个库的搜索,就已经这么多了),直到某个文件没有#include引用为止。所以,就算是最简单的一个hello程序,也会被预处理展开成几千行。

2.4 本章小结

      

简单叙述了预处理的概念与作用,简单阅读并分析了hello.c经过预处理之后的文件hello.i。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器将用C语言写成的文件hello.i翻译成为目标汇编语言的hello.s的过程。

编译的作用:将字符串转化为内部的表示结构,生成语法树并将语法树转化为目标的汇编代码。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

 

图3.1 Ubuntu下编译命令

3.3 Hello的编译结果解析

我们将hello.s直接打开:

 

图3.2 生成的hello.s文件

进行分析如下:

3.3.1 常量字符串

      在文件的开头直接给出:

 

图3.3 第一个常量字符串

 

图3.4 第二个常量字符串

分别对应于原C语言程序中的"用法: Hello 学号 姓名 秒数!\n"以及"Hello %s %s\n"。值得一提的是,由于汉字无法直接显示,所以直接变为了ASCII码,显示出来很有可能是乱码的形式。

3.3.2 变量字符串(数组)

这里面的变量字符串就是我们的可变参数数组argv[]。他储存在内存单元中。

  

 

图3.5 内存之中的字符串变量

该段代码通过%rax寄存器找到当前字符串所处的位置并将其赋值入%rsi中以便于之后调用printf函数。

其实在这样的代码里,数组和指针的操作似乎难以区分,因为数组头的名字不过也是一个地址。

3.3.3 常量数字

这个代码里面的常量数字很少,都是使用立即数的引用实现的,即在数字前面加上$实现。

3.3.4 变量数字

该代码中仅有主函数的参数argc是变量整型。它记录在%edi这个寄存器中并在一开始赋值给了-20(%rbp)这个位置。

 

图3.6 argc整型变量的赋值

3.3.5 运算操作

运算操作在hello.s里面以addq或者subq的形式实现。由于仅涉及加法和减法,所以并没有太复杂。

 

图3.7 加操作

 

图3.8 减操作

3.3.6 关系操作(并跳转判断)

   关系操作在这份代码里面往往与跳转相关联。

   关系操作一般仅仅以cmp这条语句的形式出现,结果会设置在条件码之中,所以二者并起来讲比较好。

 

图3.9 关系操作与跳转

      上图是一个关系操作和与之相连的跳转。含义为如果-20(%rbp)位置的数的大小等于4,那么就跳转到.L2代码处。

      jX指令后面的X设置了跳转条件是什么,例如这里的e就意味着相等时跳转。那么类似的,还有小于时跳转,大于时跳转,也就意味着不同的关系运算符和其对应的跳转方式。

3.3.7 函数

      在hello.s之中出现了多个函数。例如,我们的程序主体本身就是在main函数里面实现的。再比如,我们后面用到的printf、sleep、atoi等函数。以下将一一说明。

 

图3.10 main函数的定义

       main函数的定义是在代码段前的一行定义式里面体现的。它表示之后的代码都是main函数里面的内容。

   而在代码中我们使用的库函数,例如printf、sleep等,都是通过这样的call语句来实现的:

 

图3.11 exit函数的调用

 

图3.12 printf函数的调用

          其他的函数类似。

但是我们发现,这样调用函数似乎遗漏了我们传向这个函数的参数。不得不提及的是传向参数的参数都使用同一套规则存放在寄存器里面。第一个参数存放在寄存器%rdi中,第二个参数存放在寄存器%rsi之中……以下是详细的参数表。

1

2

3

4

5

6

7以后

%rdi

%rsi

%rdx

%rcx

%r8

%r9

内存中

下面举例说明以下几个函数的调用。

 

图3.13 printf函数及其参数

我们按照上表中的顺序查看其参数。首先是%rdi寄存器,存放了.LC1处字符串”hello %d %d\n”,这是printf函数的第一个参数,接下来是两个变量字符串,前面提到过,其存放在内存之中,直接使用(%rbp)加上长度来引用。这里的操作利用了%rax来作为中间变量计算内存位置。

 

图3.14 getchar函数调用相关仅有一行

接着是无参数的函数getchar。我们发现其只存在一行,所以在.s文件中的体现仅有一行调用。

3.3.8 类型转换

在这份代码中仅涉及一处类型转换,那就是使用atoi函数将字符串转换成了数字。由于atoi函数的具体实现并未在hello.s之中体现,所以我们并不讲解。

3.4 本章小结

利用gcc指令进行了文件的初步编译产生了hello.s文件,并初步解析了hello.s之中的汇编代码对应的各个C语言指令和操作。主要解释了变量常量、跳转以及函数的汇编实现。目前的hello程序以及变成了更加底层的语言。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:编译器将.s文件翻译成为机器指令二进制程序,并且形成.o文件,它叫做可重定位目标程序文件。

汇编的作用:生成计算机可以理解的语言,为之后的链接操作做了准备工作。

4.2 在Ubuntu下汇编的命令

 

汇编指令:as hello.s -o hello.o

4.3 可重定位目标elf格式

    首先使用指令readelf -a -W hello.o > hello.elf得到hello.o的elf文件格式。

       我们直接在Ubuntu系统下打开该文件。我们可以发现文件被其前面的标签分解成了几个部分。

       4.3.1 elf

             

 

图4.1 elf头

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

    4.3.2 节头部表

 

图4.2 节头部表

节头部表记录了.elf文件之中出现的所有节的信息,包括节的地址、偏移量、大小等。Address一栏即为地址、off栏即为偏移量、Size栏为大小。

而在下面的key to flags一栏里面讲解了Flg栏位中的符号代表的含义。

    4.3.3 .rela.text

 

图4.3 .rela.text节

.rela.text节存放着当前代码的重定位条目,也就是需要和其他文件链接的部分。例如,我们在hello.c文件中调用了puts、exit、printf、atoi等函数,而这些函数是存放在C语言标准库里面的,并未在我们的原程序中出现,所以需要链接。果不其然,我们在.rela.text节右边的Symbol’s Name这一栏里面找到了我们调用的这些函数。

而其他栏信息的含义分别为:

Offset——偏移量,需要进行重定向的代码在.data节中的偏移位置。

Info——信息,包含2个部分前半段代表重定位到的目标在.symtab里面的偏移量,后半段代表重定位的类型。

Type——类型,重定位到的目标的类型。

Addend——一个调整值,对被修改引用的值做偏移调整。

       4.3.4 .rela.eh_frame

 

图4.4 .rela.eh_frame节

这一节记录了.eh_frame节的重定位信息,而.eh_frame节的作用是处理异常

       4.3.5 symtab节(符号表)

 

图4.5 .symtab节

这一节存放了在程序中定义和引用的函数和全局变量的信息。其中栏信息的含义为:

Value——偏移量,距离定义目标的节的起始位置的偏移量。

Size——目标的大小。

Type——类型,通常不是数据就是函数。

Bind——表示符号是本地符号还是全局符号。

Vis——访问方式,在这里都是默认。

Ndx——符号类型,UND代表未被定义,ABS代表不应被重定位。

4.4 Hello.o的结果解析

 

图4.6 hello.o的反汇编命令

4.4.1 机器语言的构成

机器语言主要构成是许多许多的二进制数,它是机器可以直接执行的语言。在人类阅读时通常转化为16进制数并且两个两个一组。一组16进制数能够解释一个最小的机器对应的原子操作。

机器语言由三种数据构成。一是操作码,它具体说明了操作的性质和功能,每一条指令都有一个相应的操作码,计算机通过识别该操作码来完成不同的操作;二是操作数的地址,CPU通过地址取得所需的操作数;三是操作结果的存储地址,把对操作数的处理所产生的结果保存在该地址中,以便再次使用。

4.4.2 机器语言与汇编语言的映射关系

对hell.o进行反汇编之后,我们发现其与第三章我们输出的并无很大的不同,不同主要体现在以下几个方面:

  1. 分支转移

 

图4.7 机器语言反汇编的分支转移

在hello.s中,分支转移的跳转大多使用.L1,.L2这样的节代号来跳转,但是在hello.o的反汇编文件之中我们的分支转移直接跳转到的是指令地址,例如上图中的2f。背后的原因当然是由于机器只能识别二进制地址而非汇编语言中的语句

  1. 函数调用

 

图4.8 机器语言反汇编的函数调用

在hello.s中,函数调用的语句是通过call+函数名来实现的,而在hello.o的反汇编文件中我们看到其call后面直接跟着的是下一条指令的地址,因为目前我们使用的函数都还未被重定位,所以要等待链接之后才确定。

  1. 访问字符数组常量

 

图4.9 机器语言反汇编的字符常量

在hello.s中,我们访问字符串常量是在头部预定义了一个.L0,并在调用时使用.L0(%rsp)调用,在反汇编文件中,我们看到是0x0(%rip),这是由于还没有重定位,要等到链接之后确定地址。

4.5 本章小结

简单查看了hello.o的elf格式,为之后的链接做了铺垫。对hello.o的反汇编文件进行了阅读并且将其与hello.s进行了比较,简单了解了机器语言与汇编语言之间的映射关系。

第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 Ubuntu下链接的命令

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

       首先使用readelf来列出其各段基本信息:

      

 

图5.2 Ubuntu下使用readelf查看hello的elf格式

    这样之后在Section Headers这一栏就可以看到各节的信息:

 

图5.3 各节的信息

        其中Address是起始地址,Off是其偏移量,Size是各节的大小。

5.4 hello的虚拟地址空间

    使用edb加载hello,我们查看data dump的内容,发现程序加载的地址是从0x401000到0x402000的。遗憾的是,出现了乱码的内容。

 

图5.4 使用edb查看data dump

但是我们仍然可以对应图5.3的内容来查看各节所在的位置。例如,找到0x401020就可以找到.plt节所在的位置。

 

图5.5 .plt本应在的位置

5.5 链接的重定位过程分析

       使用命令objdump -d -r hello> output.txt

 

图5.5 反汇编hello的命令

       对于hello.o和hello来说,两者main函数的汇编指令完全相同,除了地址由相对偏移变成了可以由CPU直接寻址的绝对地址。链接器把hello.o中的偏移量加上程序在虚拟内存中的起始地址0x400000和.text节的偏移量就得到了hello中的地址。

       5.5.1 分支转移

              我们找到之前在第四章中hello.o里面的代码,再次查看它:

             

 

5.6 反汇编hello后的分支转移

发现je后面的地址已经不再为0,而是401154一个确定有效的地址了。由偏移量变为了偏移量+函数的起始地址。

       5.5.2 函数调用

              找到之前在第四章中函数调用对应的代码:


       5.7 反汇编hello后的函数调用

 

其调用地址已经变为了一个具体的值。此时动态链接库中的函数已经加入到了PLT中,.text.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt.got.plt

       5.5.3 字符串引用

              找到之前在第四章中字符串引用对应的代码:

 

5.8 反汇编hello后的字符串引用

链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。

除了main函数,hello的反汇编文件比hello.o的反汇编文件多出了几个函数:printfsleepputsgetcharatoiexit。除了.text节的区别外,hello1.obhello.ob多出了几个节:.init节、.plt节、.fini节。其中.init节是程序初始化需要执行的代码,.fini节是程序正常终止时需要执行的代码,.plt节是动态链接中的过程链接表。

5.6 hello的执行流程

子程序名如下:

ld-2.27.so!_dl_start

ld-2.27.so!_dl_init

hello!_start

libc-2.27.so!__libc_start_main

libc-2.27.so!__cxa_atexit

libc-2.27.so!__libc_csu_init

libc-2.27.so!_setjmp

hello!main

hello!puts@plt

hello!exit@plt

hello!printf@plt

hello!sleep@plt

hello!getchar@plt

ld-2.27.so!_dl_runtime_resolve_xsave

ld-2.27.so!_dl_fixup

ld-2.27.so!_dl_lookup_symbol_x

libc-2.27.so!exit

5.7 Hello的动态链接分析

在.got.plt节中,我们存放了有关动态链接的参数,其在dl_init前后的内容发生了突变,其中一个地址指向的内容是重定位表,另一个指向的是动态链接器运行的地址。重定位表可用来确定调用函数的地址,动态链接器则是进行动态链接。

5.8 本章小结

简单了解了hello.o变成hello可执行程序时的链接工作,熟悉了链接、反汇编的指令,并且对如何链接、如何运行进行了一定程度的熟悉。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。

进程的作用:进程提供了一个假象,似乎每一个程序都能够独占CPU而运行,并且作为独立运行的基本单位接受统一的进程调度

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

作用:壳作为操作系统和用户交互的一个接口,可以代替用户执行各种命令以及执行它内置的命令。

处理流程:首先读取用户的命令,接着对命令进行分析:是内置命令还是运行命令?若是内置命令则直接用对应的方式来运行,如果是运行命令,那么就为其分配子进程并运行指定的程序。并且,壳还应当对错误输入以及键盘信号进行合适的响应。

6.3 Hello的fork进程创建过程

       当我们在shell里面输入./hello的命令时,shell判断其为运行其他程序的语句,于是进行了一次fork()函数。这样就创建了一个子进程。创建子进程之后,子进程相当于是父进程的一个副本,那么这就意味着它可以读写父进程以及打开或者读取的任何文件。但是这时的子进程和父进程并非完全相同,因为我们需要让子进程来运行我们的hello程序而父进程由于我们选择的是前台运行所以需要等待,也就是使用waitpid()函数来等待子进程中的程序运行完毕。

6.4 Hello的execve过程

在使用fork()函数创建了子进程之后,我们需要让子进程加载并运行hello这个程序,于是我们需要使用execve()函数。execve()函数调用一次却从不返回,其只在发生错误时会返回到程序中,例如文件不存在的情况下。并且,它会覆盖当前进程的代码、数据、栈,并且保留相同的PID来运行我们加载的程序。

加载并运行hello需要以下几个步骤:

(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。

(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

(3)映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。

6.5 Hello的进程执行

进程上下文,意思是可执行程序代码,是进程的重要组成部分。这些代码从可执行文件载入到进程的地址空间执行。一般程序在用户空间执行当一个程序调用了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文。上下文保存的流程是:1.保存当前的上下文2.恢复某个先前被抢占资源的进程保存的上下文3.将控制传递给这个新恢复的进程。

时间片就是设立的一个时间界限,每次只要时间到,就换一个进程来执行。相当于一个定时闹钟的作用,从而实现似乎每个进程都在独占CPU的效果。

那么结合目前的hello程序来进行讲解:hello在刚开始运行时保存了一个上下文,其在用户模式下进行。如果没有任何中断或者信号发出,那么它将会一直正常运行下去,这中间它可能会因为进程时间片的缘故和其他进程交换,但是并不影响我们的“观感”,它看起来是一直在运行的。

如果我们在这个过程中使用键盘输入,那么当前的上下文就会被保存,接着切入到内核模式,交由操作系统来处理信号,或者是让用户自己定义的信号处理系统来处理。之后再根据信号种类来决定是继续执行还是停止。

当程序在执行sleep函数时,系统调用显式地请求让调用进程休眠,调度器抢占当前进程,记录下当前的上下文,并发生上下文切换,将控制转移到新的进程,此时计时器开始计时。当计时器达到我们输入的时间时,产生中断信号中断当前进程,控制回到hello进程。

当程序执行getchar函数时,发生上下文切换,进入内核模式请求来自键盘缓冲区的输入,并执行上下文切换使其他进程运行。获得输入之后,控制回到hello进程中。

6.6 hello的异常与信号处理

       hello执行过程中可能出现三类异常:陷阱、故障和终止。

       陷阱是执行指令的结果,例如打开文件的open()函数或者产生子进程的fork()函数。故障是非有意的结果,但是可以被修复,比如对于数组的越界访问。而终止则是非有意发生的致命错误,常常不可修复。

       会产生的信号有许多许多种,他们都以SIG开头,代表着不同的含义,比如:SIGINT是来自键盘的终止信号,SIGKILL是杀死程序的终止信号 ,SIGSEGV是由于段错误原因终止程序的信号,SIGALRM是由于计时器终止程序的信号,SIGCHLD 是子进程停止发出的一个信号,一般被忽略。

       以下是hello对一些动作的反应:

 

图6.1 在hello执行过程中乱按

        当在hello执行过程中不停乱按时,我们发现hello程序对此并无反应,执行结束之后也并没有特殊情况。

 

图6.2 在hello执行过程中键入Ctrl+C

        当在hello执行过程中键入Ctrl+C时,我们发现程序立即停止了,使用ps命令查看也并没有进程存在,说明hello程序直接被Ctrl+C发出的SIGINT信号给终止了。

 

图6.3 在hello执行过程中键入Ctrl+Z

       当在hello执行过程中键入Ctrl+Z时,我们发现出现了一行【1】+ Stopped的字样,说明hello程序已被停止。使用ps命令查看,发现进程中仍然存在hello,且pid为4075,说明进程并没有被回收,仍处于暂停状态。说明我们的Ctrl+Z发送了SIGSTP信号给hello进程并且使它成功停止了。

 

图6.4 在Ctrl+Z之后使用kill命令

        在这之后我们执行kill命令,发现给出了一行[1]+ killed后面是命令,含义为已被终止的进程。这个时候我们使用ps查看,发现hello的进程果然没有了,kill操作成功发送SIGINT信号给hello进程并使其终止。

 

图6.5 在Ctrl+Z之后使用fg命令

        在Ctrl+Z命令之后,从上文可以得知,hello进程被暂停。这时键入命令fg 1,将hello进程调回前台运行,发现hello进程确实回来执行了,并且把接下来的信息打印完毕。

6.7本章小结

本章从进程开始,了解了进程、shell之中有关hello进程的一些操作。并且了解了异常和信号。之后还阐述了两个和shell以及进程运行密不可分的函数:fork()和execve()。在Ubuntu之中实践了这些操作是怎么通过shell变成异常和信号对hello进程造成不同影响的。

第7章 hello的存储管理

7.1 hello的存储器地址空间

       物理地址:内储存器之中实际有效的地址。

       逻辑地址:指令给出的地址,实际上是一个相对地址,需要通过寻址方式的计算和变化才能得到实际的物理地址。

       线性地址:线性地址是逻辑地址到物理地址的中间层。由于逻辑地址相当于一个偏移量,线性地址就是基址加上这个偏移量。但是由于有分页方式的存在,线性地址还要经过一个变换才能变为物理地址。

       虚拟地址:虚拟地址是当CPU启动保护模式之后,物理内存映射到磁盘的一个虚拟空间,这个映射由一套硬件、软件的完整系统来实现。

       举例来说,对于hello的反汇编:

 

图7.1 hello反汇编文件中main函数的地址

       这是其中main函数的地址,这是一个逻辑地址,我们还需要和该程序的基址相加才能够得到其线性地址。这样之后,由于现代处理器的特点,还要进行虚拟地址的转换,所以,这样就得到了hello的main函数的实际地址。

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

从逻辑地址转化到线性地址是通过分段机制来进行的。程序通过分段划分为多个模块,如代码段、数据段、共享段:可以分别编写和编译;可以针对不同类型的段采取不同的保护;可以按段为单位来进行共享,包括通过动态链接进行代码共享。这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。

首先是通过某个寄存器找到这个数据段所在的段描述符,之后通过段描述符找到相应的线性地址。

这是段选择符的格式:

 

图7.2 段选择符的格式

对于段描述符,它保存了段的各种信息,包括首字节的线性地址,访问级别

 

图7.3 段描述符的格式

其中BASE是包含段的首字节的线性地址,DPL是访问权限。

于是,从逻辑地址到线性地址的变化如下图所示:

图7.4 从逻辑地址到线性地址的变化

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

线性地址到物理地址的变换是通过分页模式来执行的。线性地址分为三个字段:页目录表项指针、页表项指针和页内偏移量。控制寄存器(CR3)保存了页目录的起始地址。如下图所示,处理器在进行线性地址到物理地址的转换时,采用如下步骤:

 

 

图7.5 线性地址到物理地址的变换

从上图可以看出,线性地址的前10位是页目录项的索引,其包含了页表的基址。中间10位是页表的索引,其包含了物理内存中页面的基址。最后有12位的偏移量,与前面的页面基址相加即得到了操作数的物理地址。

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

为了消除每次CPU产生一个虚拟地址MMU就查阅一个PTE带来的时间开销,许多系统都在MMU中包括了一个关于PTE的小的缓存,称为翻译后被缓冲器(TLB),TLB的速度快于L1 cache。

如果请求的虚拟地址在TLB中存在,CAM 将给出一个非常快速的匹配结果,之后就可以使用得到的物理地址访问存储器。如果请求的虚拟地址不在 TLB 中,就会使用标签页表进行虚实地址转换,而标签页表的访问速度比TLB慢很多。

 

图7.6 用来访问TLB的索引

同时,为了减少页表太大而造成的空间损失,可以使用层次结构的页表页压缩页表大小。如果不使用层次结构,仅有一级页表的话,那么4KB的页面,48位地址空间,8字节的PTE就需要512GB的页表,开销非常恐怖。

但如果采用K级页表,虚拟地址就会被划分为KVPN。当然,四级页表也就是4VPN

 

7.7 四级页表的访址方式(Intel i7)

每个第iVPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE

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

       三级Cache是介于CPU和主存储器间的高速小容量存储器,由静态存储芯片SRAM组成,容量较小但比主存DRAM技术更加昂贵而快速,接近于CPU的速度。

       通过以上对逻辑地址到线性地址到物理地址的转换,现在我们已经拥有了我们需要访问的物理地址,接下来就是去找到他。Cache的访问是按照分块策略来进行的。总的来说,对于某一级Cache的查找,思路是这样的:如果我们需要的数据块当前已经缓存在Cache之中,那么在当前级直接取出,也就是Cache命中,否则就前往下一级寻找,也就是缓存不命中。并且,在不命中时会发生Cache中数据的替换,替换策略则有很多种,与Cache的相连方式以及写入方式都有关。

7.6 hello进程fork时的内存映射

       当fork()函数被shell进程调用时,内核为新进程创建各种数据结构,并分配一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中每个区域结构都标记为私有的写时复制。

 

图7.8 子进程与父进程的内存映射

如图,图中的共享对象其实也就是子进程,他们的虚拟内存和物理内存的关系如图所示。

7.7 hello进程execve时的内存映射

以下格式自行编排,编辑时删除

execve系统调用将当前进程重新划分,为可执行文件划出合理的空间,并将参数置于当前进程64M末端,为应用程序的执行做好了准备。

加载并运行hello需要以下几个步骤:

(1)删除已存在的用户区域。

(2)映射私有区域,为新程序的代码、数据.bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。

(3)映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

缺页其实就是虚拟内存中的不命中问题。也就是我们当前想要访问的虚拟内存地址的页并不在物理内存中。但是由于磁盘读取速度非常非常慢,所以缺页的代价也相应的非常大,所以我们并不能像缓存不命中那样简单的进行替换策略的设计,而是需要软硬件配合的方法。缺页异常的处理程序是位于操作系统内核之中的。所以,当触发缺页异常时,控制首先回到操作系统内核之中,交给缺页异常处理程序来处理,之后缺页异常处理利用其复杂的替换策略计算出应该替换掉哪一页,并且采用写回策略。这样以后,控制回到之前触发缺页异常的进程,继续执行。

7.9动态存储分配管理

所谓动态内存分配(Dynamic Memory Allocation)就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

当程序运行到需要一个动态分配的变量或对象时,必须向系统申请取得堆中的一块所需大小的存贮空间,用于存贮该变量或对象。当不再使用该变量或对象时,也就是它的生命结束时,要显式释放它所占用的存贮空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。

寻找一个空闲块的方式有三种:

(1)首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块:可以取总块数(包括已分配和空闲块)的线性时间,但是会在靠近链表起始处留下小空闲块的“碎片”。

(2)下一次适配:和首次适配相似,只是从链表中上一次查询结束的地方开始,优点是比首次适应更快:避免重复扫描那些无用块。但是一些研究表明,下一次适配的内存利用率要比首次适配低得多。

(3)最佳适配:查询链表,选择一个最好的空闲块适配,剩余最少空闲空间,优点是可以保证碎片最小——提高内存利用率,但是通常运行速度会慢于首次适配。

同时,为了减小内存空间的碎片,还需要使用链表来对某些块来进行合并。

7.10本章小结

从程序中的地址表示一直整理到了硬件层次对于地址的表示以及他们之间的链接。并且简要介绍了操作系统和硬件对于Cache、内存、磁盘空间的管理策略。并且针对hello程序中的一些函数,分析了他们是如何运用内存,管理内存的。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

 

图8.1 Unix IO接口及函数

8.3 printf的实现分析

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

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

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

8.4 getchar的实现分析

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

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

8.5本章小结

本章介绍了Linux中I/O设备的管理方法,UnixI/O接口和函数,并且分析了printf和getchar函数是如何通过UnixI/O函数实现其功能的。

结论

hello的一生如下:

首先,他从键盘上诞生于我们的编辑器之中,形成了hello.c。

之后预处理器将其预处理为hello.i,这份代码已经做好了汇编的准备。

编译器又将hello.i编译为hello.s,汇编器又将其汇编成为hello.o。

之后链接器将hello.o与他利用到的动态链接库相链接成为了hello可执行文件,至此,hello已经变成可以运行的程序了。

打开shell,键入./hello 7203610508 yangyizheng,shell为hello使用fork创建子进程并使用execve加载hello程序运行。

在hello执行前,CPU为其分配时间片,hello按顺序执行自己的指令。

当hello运行,CPU为其申请了一个虚拟空间,把虚拟地址转换成为物理地址并访存。

当hello访问内存时,函数向堆栈中申请动态的访问。

在运行途中,hello可能会经历各种各样的信号与异常,有来自我们键盘的键入,也可能只是简单地与其他程序进行切换,也可能是发生了错误内核要求其中止。。。

最后,hello成功运行,shell父进程对其进行回收,hello的一生结束了。

通过对hello一生的学习与分析,我实践了CSAPP这本书讲授的内容,感悟到了程序也是有他的一生。即使是一个简单的hello程序,也需要无数处理程序的支持,也需要一个庞大而复杂的管理体系,比如内存管理体系、信号处理体系。这些都是前人经过无数的努力研究出来的。

附件

       hello.i: .c文件经过预处理之后的文本文件。

hello.s: hello文件编译之后的汇编文件。

hello.o:  hello文件汇编之后形成的可重定位目标执行文件。

hello: 链接之后生成的可执行文件。

output.txt:  hello可执行文件的反汇编文件,查看其汇编代码。

output0.txt:  hello.o可重定位目标执行文件的反汇编文件。

hello.elf:hello.o文件的elf格式。

参考文献

[1]百度百科:进程. https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503

[2]    CSDN:进程.https://blog.csdn.net/weixin_57675461/article/details/123616847

[3]  深入理解计算机系统

[4] 百度百科:地址

[5] CSDN:从逻辑地址到线性地址从逻辑地址到线性地址_多多是小坏熊的博客-CSDN博客_逻辑地址到线性地址

[6]CSDN:程序人生

[HITICS] 哈工大2019秋CSAPP大作业-程序人生-Hello’s P2P_北言栾生的博客-CSDN博客

[7]百度百科:动态内存分配

https://baike.baidu.com/item/%E5%8A%A8%E6%80%81%E5%88%86%E9%85%8D%E5%86%85%E5%AD%98/2968252

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值