哈尔滨工业大学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'转变的过程。在经过一系列软件处理后,在 shell 环境中将 Hello 程序作为 C 语言源代码保存于一个文本文件。通过预处理、汇编、编译以及链接等步骤进行处理,最终生成了一个可执行文件。当我们在shell中输入相应的命令启动该可执行文件后,在 shell 会创建子进程来处理 Hello 的运行。此时 shell 会创建子进程来处理 Hello 的运行需求。这一过程总结下来就是 Hello 程序如何如何完成向进程中转变的具体实现路径
在Hello程序的020过程中,在Hello进程中未启动前及启动后均未占用任何系统资源。当启动该 Hello 进程时(或说是被启动时),系统为其分配内存块和时间片 slice。然而,在该进程退出并完成所有任务后(例如接收到终止信号或自然退出),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.h >库中的相关内容,同时也在代码中取消了所有通过#define声明的语句.此外,在需要时还可以通过#if来控制后续代码的处理流程.
经过这样的处理后, 程序将变得更加易于理解和维护, 并且在调整. 优化以及移植与调试方面表现得更加简便. 此外, 在遵循模块化原则的程序设计方法下也能显著提升效率.
2.2在Ubuntu下预处理的命令

图2.1 Ubuntu下预处理命令
预处理命令:cpp hello.c>hello.i
2.3 Hello的预处理结果解析

图2.2 Hello的预处理结果
我们采用文本编辑器打开hello.i文档,并发现在其中展开并包含了数百行代码,在文件末尾附近找到了主函数,并未发生明显的变化。

图2.3 Hello预处理的库引用查找
我们可以肯定地知道,在我们所包含的源代码中的库函数中还包括无数个类似的include指令。因此我们的预处理过程采用了图示法来展示其工作流程(主要针对标准输入输出函数库(stdio.h)),直至遇到一个不带include头文件声明的文件时停止。由此可见即使是最简单的hello程序也会被展开成数千行代码。
2.4 本章小结
概述了预处理的概念及其作用,并详细研究了经过预处理后生成的文件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 第二个常量字符串
功能说明:Hello 学号 姓名 秒数!\n
3.3.2****变量字符串(数组)
内部的变量字符串即我们所说的参数数组argv[]它们存储在内存单元中

图3.5 内存之中的字符串变量
这段代码利用%rax寄存器确定当前字符串的位置后将该位置信息传递至%rsi寄存器以便后续调用printf函数
在这种代码中进行操作时(In this code context when performing operations), 区分数组与指针的行为并不容易; 因为(because)数组的头部名称实际上就是一个内存地址.
3.3.3****常量数字
这个代码中的少量常量数字采用立即数引用方式实现较少
3.3.4****变量数字
该代码中仅有一个函数体内的参数argc属于变量类型整型。它位于%edi寄存器内并被初始化为-20(%rbp)的位置。

图3.6 argc整型变量的赋值
3.3.5****运算操作
运算功能在hello模块中以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"这一字符串(此处%d表示占位符),该参数即为打印信息时使用的第一个输入字段。随后还有两个自定义的字符串常量(如前所述),它们都位于内存中,并通过加载基址寄存器%rbp并结合其长度属性来定位这些内存区域。这里的操作利用了%rax来作为中间变量计算内存位置。

图3.14 getchar函数调用相关仅有一行
依次处理的是无参数函数getchar。经过分析发现该函数仅占据一行代码空间,在.s文件中对该函数的引用也仅限于一行调用。
3.3.8****类型转换
这份代码中仅有一个地方进行了类型转换, 也就是使用 atoi 函数将字符串转换为数字. 因为 hello.s 中未展示 atoi 函数的具体实现细节, 我们不做深入讲解.
3.4 本章小结
通过gcc指令对目标文件进行初步构建生成了hello.s,并对其中的汇编代码与相关C语言指令及其操作进行了初始分析。重点阐述了变量与常量的定义、程序流程中的转向机制以及函数在汇编层面的具体实现。目前构建完成后的程序已转换为更为基础的操作方式。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:在软件开发中, 编译器负责将带有汇编码的目标代码文件(.s文件)转换为机器级指令构成的二进制执行程序, 并生成对应的可执行文件(.o文件), 这种过程被称为可重定位目标程序生成过程
汇编的功能体现在将高级-level语言转换成计算机能够识别并执行的指令序列,并在此过程中确保后续的数据传输和引用能够顺利执行。
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头
首先是由一个 16 字节长的 Magic 序列开头形成的 elf 标识符部分。它指出了生成该文件所需的系统字大小以及数据在内存中的顺序。剩余的部分则提供了关于解析目标文件所需的技术细节和其他相关信息。其中包含了 elf 标识符部分的总长度、所解析的目标文件类型、所使用的机器指令集以及数据段起始位置的信息;此外还包含了程序运行时所需的各种段落条目及其数量等详细信息。
4.3.2****节头部表

图4.2 节头部表
该表列明了elf文件中所有存在的节及其相关信息。具体包括每个节的地址、offset值以及大小值等信息。其中:Address一栏即为此表中的地址字段,“off一栏则对应offset值字段,“Size一栏则表示数据大小字段。
而在下面的key to flags一栏里面讲解了Flg栏位中的符号代表的含义。
4.3.3**.rela.text****节**

图4.3 .rela.text节
.rela.text字段记录了当前代码中的重定位信息。
即使是一个项目内部不同模块之间也需要进行接口对接。
例如,在hello.c文件中使用了putchar、exit以及printf和atof等系统函数。
这些系统函数位于C语言标准库中,并不在我们的原始程序里。
因此必须与外部建立连接
而其他栏信息的含义分别为:
Offset——偏移量,需要进行重定向的代码在.data节中的偏移位置。
Information——信息, consist of 两个组成部分:第一部分表示目标定位位置位于(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位一组的十六进制数字。每组十六进制数字对应一个最小的原子操作。
机器语言由三种数据构成。其中一种是操作码,它具体说明了运算的性质与功能。每条指令都对应一个特定的操作码,并可通过识别该代码来执行不同的计算;其二是处理所需的操作数地址,在中央处理器中能够根据内存或寄存器中的地址获取所需的数值量;其三是将运算结果存储至指定地址,并将计算所得的数据按要求保存在固定的位置以备后续使用。
4.4.2****机器语言与汇编语言的映射关系
在对hell.o进行反汇编操作后分析的结果显示,在整体特征上与第三章输出的内容基本一致
- 分支转移

图4.7 机器语言反汇编的分支转移
在主要集中在hello.s环境中进行分支转移操作时,默认会采用.L1、.L2等标记符来进行跳跃操作,在反汇编文件构建过程中对应的分支转移操作则直接指向具体的操作指令位置(如图所示)。这种差异的根本原因在于机器仅能识别二进制地址而无法处理汇编语言中的语句结构
- 函数调用

图4.8 机器语言反汇编的函数调用
在hello.s文件中,默认情况下,默认情况下,默认情况下,默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认情况下默认
- 访问字符数组常量

图4.9 机器语言反汇编的字符常量
在hello.s源代码中,在头部我们预先定义了一个标记名称为L0的虚拟机字节位置,并通过该标记位置的值来引用字符串常量,在函数调用时通过L0(%rsp)进行引用操作。而在反汇编文件中查看到的是内存偏移值为零(%rip)的情况,则是因为此时还未完成重定位操作,在程序链接完成后才能确定具体地址位置信息。
4.5 本章小结
初步研究了...并为后续进行链接处理做好了准备。详细阅读并对比...进一步明确了机器语言与汇编语言之间的对应关系。
第5章 链接
5.1 链接的概念与作用
概念上
链接的功能在于通过链接器使分 compilations 成为一种可行的方法;而我们可以将其划分为更为精炼且易于管理的部分;每个部分都可以单独进行更新与重新编译;当我们在修改其中一个部分时
5.2 在Ubuntu下链接的命令
该命令用于执行动态链接器并完成指定的目标链接操作

图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的内容来确定各节的具体位置信息。比如,在内存地址...处可以直接定位到.

图5.5 .plt本应在的位置
5.5 链接的重定位过程分析
使用命令objdump -d -r hello> output.txt

图5.5 反汇编hello的命令
对于hello.o以及其对应的hello程序来说,在main函数层面它们采用相同的汇编指令序列;唯一的区别在于前者使用的地址是基于相对偏移的方式而后者采用了CPU可以直接访问的绝对地址系统。在连接过程中链接器会将hello.o文件中的偏移量与主程序运行时所处虚拟内存空间的起始位置(固定值为0x400000)以及目标text节内部所定义的偏移量进行求和计算从而确定最终的实际内存地址位置。
5.5.1****分支转移
**** 我们找到之前在第四章中hello.o里面的代码,再次查看它:

图5.6 反汇编hello后的分支转移
确认je后面所使用的内存地址不再是零值。该偏移量已被转换为加上目标函数的起始内存地址。
确认je后面所使用的内存地址不再是零值。该偏移量已被转换为加上目标函数的起始内存地址。
5.5.2****函数调用
找到之前在第四章中函数调用对应的代码:

图5.7 反汇编hello后的函数调用
其调用地址已变为具体数值。此时动态链接库内的函数已整合至 PLT 中, .text 和 plt 节之间的相对距离已确定,通过计算两个程序文本段之间的相对位置,会将这些程序文本段中的函数引用位置调整为相对于 PLT 的偏移量,从而实现重定位后的正确引用关系.特别地,此类重定位的 Linker 会构造 .plt 和 .got=plt 的形式.
5.5.3****字符串引用
找到之前在第四章中字符串引用对应的代码:

图5.8 反汇编hello后的字符串引用
链接器在解析重定位条目时识别到两个类型为R_X86_64_PC32的目标地址,并发现它们分别对应printf函数中的两个字符串常量存储位置(即.rodata中的两个字符串)。随后系统计算这两个内存区域之间的相对距离,并根据此信息直接将调用后的目标地址值设置为目标地址与下一条指令起始地址之差,并对该位置进行标记指向相应的字符串常量存储位置。
除了main函数外,hello反汇编文件相较于hello.o反汇编文件在功能模块上增加了printf、sleep、puts、getchar、atoi和exit等几个标准库函数。除了存储结构上的差异(.text节区别),hello1.ob相较于hello.o则多了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前后动态链接内容发生了显著变化。其中某个地址指向的内容是重定位表(RSP),而另一个地址所指的则是动态链接器运行时使用的内存地址。重定位表可用来确定调用函数所需的内存地址(如图3所示)。它负责将程序中的静态内存地址转换为运行时的内存地址。
5.8 本章小结
初步掌握了将hello.o对象文件转换为最终hello.exe可执行程序的编译连接操作,并深入学习并掌握了编译连接指令及其反汇编技术。同时,在实践中熟练掌握了基本的操作方法和流程。
第6章 hello进程管理
6.1 进程的概念与作用
进程可被视为一个独立功能的程序在特定数据集合上执行的一次运行活动。该过程不仅具备独立性还与特定的数据集合紧密相关。作为一个动态的概念进程不断变化并且是一个实体性的操作单元。它不仅包含程序代码本身还涵盖当前的操作状态即由程序计数器值及处理寄存器内容来体现其状态。
进程的功能:进程模拟出一个假象,让每个程序独自占用CPU运行,并将它们视为一个独立运行的基本单元由操作系统的统一调度处理
6.2 简述壳Shell-bash的作用与处理流程
作用:shell充当操作系统与用户交互的入口,并允许用户运行各种系统命令以及其内部预设的命令。
处理流程:首先获取用户的指令;随后会对指令进行分类判断:确定是否为系统内建指令或外部执行指令?如果是系统内建指令,则可以直接采用相应的方式处理;而对于外部执行指令,则会独立创建子进程来执行相应的程序;此外,在处理过程中还需要妥善应对用户的错误输入以及各种操作中断事件。
6.3 Hello的fork进程创建过程
当我们在shell执行./hello命令时(或曰:当我们输入./hello并触发该命令执行),shell将识别该命令作为需要运行其他程序的指示(或曰:将该命令视为可能启动其他程序的操作),随后立即调用fork()函数(或曰: shell会立即触发fork()动作)。这个动作的结果是生成了一个子进程(或曰:创建了一个新的 shell 进程)。这个新产生的子进程中并不存在完全复制父进程中所有资源的状态(或曰:此子进程中并未包含父进程中全部资源),它本质上是父进程的一个复制体(或曰:相当于对父过程的一个镜像)。这个复制体能够读取与写入到与父进程中相同的文件以及打开并读取的所有内容(或曰:此复制体会继承自父进程中所有的文件访问权限与路径指向)。然而此时生成的子进程中并不存在完全复制父进程中所有资源的状态(或曰:此状态下生成的子过程并非完全等同于父过程),因为我们需要让此新产生的 Child 子进程中运行我们的 hello 程序(或其他所需应用程序);而同时由于我们选择了将 Child 子过程设置为前台运行的方式(或其他特定界面方式),因此Parent主进程中将不会立即退出;而是需要等待Child 子程式的执行完毕(或其他操作完成),这一步操作通常会通过调用waitpid()函数来实现。
6.4 Hello的execve过程
通过调用 fork() 函数生成了子进程后,则需要求该子进程执行 hello 程序。为此目的,则需调用 execve() 函数。然而每次调用 execve 都不会返回控制权直到出现错误为止,在这种情况下才会重返主程序。具体而言,在文件不存在的情况下则会触发此行为。值得注意的是 execve 不仅会继承父进程的所有数据与代码路径,并且还会保存相同的 PID 值以便后续运行。
加载并运行hello需要以下几个步骤:
(1)清除现有用户区域。从当前进程虚拟地址空间中的用户区字段中清除记录信息。
(2)将新程序的代码、数据、bss及栈区域进行映射。创建新的私有区域结构,并均为私有且采用复制的方式处理这些新区域。其中的text及data区对应于hello文件中的相应部分;bss区请求二进制零并被映射到匿名文件中;此外还包含在hello中;栈与堆均采用请求二进制零的方式初始化长度为零。
(3)将共享区域映射至用户虚拟地址空间中。当hello程序与共享对象建立连接时,则这些共享对象将被动态连接至该程序中,并进而被映射至用户虚拟地址空间中的共享区域内。
初始化程序计数器;配置当前进程的上下文信息中的程序计数器使其指向代码入口点;在下次调度该进程时它将从该入口点开始运行
6.5 Hello的进程执行
进程上下文即为可执行程序代码构成的一个运行环境,在操作系统中扮演着重要角色。这些代码会被加载至进程中所占据的内存区域进行执行操作。通常情况下,在用户空间运行的是那些既未调用系统调用又未触发异常的应用程序;一旦它们进行了系统性操作,则会切换到内核空间运行状态。此时我们称内核处于代表进程的状态,并认为其正处在该进程中所处的状态下。整个状态转换过程包括以下几个步骤:首先需要完成以下三个步骤:一是保存当前的状态;二是恢复被抢占资源的相关过程;三是将控制权转移给该新恢复的过程。
时间片是划分出来的CPU使用时间片的界限。每当预定的时间到达时,则切换到另一个进程来进行处理。类似于一个定时器的功能,从而使得每一个进程看似都能独自占有CPU的时间片。
为了更好地理解这一机制,请结合目前的Hello程序进行详细讲解。当Hello程序启动时,在内存中会创建一个专门用于存储当前执行状态的数据结构。其操作系统采用用户态设计,在用户模式下进行操作以确保资源的有效利用。当系统未检测到任何中断事件或控制信号时,该程序将保持正常的执行状态而不受外部干扰。在此期间,该程序可能会因处理多个子进程的时间片切换而与其它进程进行短暂的数据交换。这种现象并不会对我们观察到的结果产生任何影响。从外部观察者的视角来看,在此状态下Hello程序的行为表现得如同一个持续运转的过程。
当我们在该过程中使用键盘输入时
当程序调用睡眠函数(Sleep)执行时,在操作系统内核中会向正在运行的任务发送休眠请求。随后的操作系统调度机制会强行抢占当前运行的任务资源,并记录下其正在进行的状态信息。接着系统将执行上下文切换操作,在新任务上恢复暂停点位置并继续执行相应操作。在此过程中会启动一个计数器进行时间测量。一旦该计数值达到用户所指定的时间限制后就会触发一个中断信号以终止当前被占有的资源并将其返回给主循环(Hello)。
在执行getchar函数的过程中,在发生上下文切换的情况下(即触发了上下文切换),系统会向内核发送自键盘缓冲区的输入请求,并因此导致其他进程得以继续执行。当接收到输入后(即获得输入之后),系统会将控制权返回到hello进程的状态中。
6.6 hello的异常与信号处理
hello执行过程中可能出现三类异常:陷阱、故障和终止。
陷阱是由执行指令所导致的结果。
例如,在进行文件操作时会调用open()函数,在启动新进程时会使用fork()函数。
故障是非故意的操作所带来的结果,并且是可以被修复的,
例如,在进行数组越界访问时就会遇到此类问题。
而终止则是一种无法避免且会导致系统崩溃的严重错误,
通常无法进行修复或补救。
会产生多种类型的信号,在操作系统的日志记录中都会标记为以SIG开头的信息条目。这些标识符各自代表不同的含义,在实际应用中起到着重要的作用。例如: SIGINT代表键盘终端请求; SIGKILL是用于终止程序执行的特殊指令; SIGSEGV是由段错误引发的程序终止指令; SIGALRM是由系统定时器引发的程序停止指令; SIGCHLD 则是在子进程停止时向主进程发送的一个通用提示信息。
以下是hello对一些动作的反应:

图6.1 在hello执行过程中乱按
当在hello运行过程中出现异常时,在经过详细排查后发现该程序对此无响应,并经测试,在任务完成时也没有出现异常情况

图6.2 在hello执行过程中键入Ctrl+C
当在hello程序运行中按下Ctrl+C键时

图6.3 在hello执行过程中键入Ctrl+Z
1
1
1
当在Hello程序运行中按下Ctrl+Z时,在控制台中观察到一行显示为【1

图6.4 在Ctrl+Z之后使用kill命令
随后我们运行kill命令。观察到一行显示[1]+ killed,并指出这一行为代表已被终止的进程。此时我们调用ps命令来查看系统状态。确认hello进程已彻底消失。此次kill操作通过发送SIGINT信号使hello进程成功终止。

图6.5 在Ctrl+Z之后使用fg命令
当执行了Ctrl+Z命令后
6.7本章小结
本章从Process入手学习了Process、Shell中与Hello Process相关的操作,并学习了错误与信号。随后详细介绍了Fork()与Execve()这两个在Shell及Process运行中缺无法分割的关键函数。最后在Ubuntu环境中进行了上述操作,并观察它们如何通过Shell转换为错误与信号对Hello Process产生不同的影响。
第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 线性地址到物理地址的变换
如图所示, CPU中的线性地址前10位编码了页目录项指针,该编码值包含了主存储器起始地址的信息.中间10位编码了页表中的虚拟地址偏移信息,其中所存储的是物理内存起始地址的位置.剩余最低12位作为操作数偏移量,并与前面计算出的主要部分相叠加得到操作数的实际物理地址
7.4 TLB与四级页表支持下的VA到PA的变换
为了减少每次CPU生成虚拟地址MMU时查询PTE所导致的时间消耗,许多系统都设置了专门用于存储与PTE相关联的小型缓存;这些系统通常将此类缓存称为后向翻译缓冲区(TLB),其中TLB运行速度远超L1缓存。
如果请求的虚拟地址位于TLB中存在,则CAM能够迅速地完成匹配操作,并随后使用得到的物理地址来访问存储器。当所请求的虚拟地址不在TLB中时,则必须使用标签页表来进行虚实地址转换,并且其访问速度较之于TLB而言显著较低。

图7.6 用来访问TLB的索引
为了降低由于页面过大而导致的空间浪费的同时, 也可以通过采用分层索引策略来优化页面布局以节省内存资源. 如果没有采用分层索引策略, 则单级目录项(PTE)为8字节的情况下需要构建一个占用512GB内存的空间表格. 这样的开销在当前系统资源下显得尤为巨大.
如果使用K阶页表,则虚拟地址会被划分为K个VPN;同样地,在四级页表中对应着4个VPN。

图7.7 四级页表的访址方式(Intel i7)
每个第i个VPN都可以表示为对应到其所属的第i级页表。
在第j级页表中,每一个PTE都会均指向对应的下一级(即j+1级)页表的具体基址。
第四级page table中的每一个entry text element (PTE)会包括某一个physical page对应的ppn标识符;此外,在这种情况下也可能对应于某一个磁盘块的位置信息。
为了构建出完整的physical address映射关系,在MMU系统中需要先确定出所有的ppn标识符之前,
memory management unit必须依次访问并解析四个相应的entry text elements。
7.5 三级Cache支持下的物理内存访问
三级Cache位于CPU与主存储器之间的一种高速小容量存储器,并主要由静态存储芯片SRAM构成。其容量较小,在价格和速度方面均优于主存DRAM技术,并运行速度非常接近CPU的速度。
在完成了逻辑地址至线性地址至物理地址的转换操作后,在存储层实现了对所需物理内存地址的有效定位。Cache系统采用分块方式实现数据访问,在完成一次寻址操作后将结果反馈至主存储器中以供后续使用。对于某一级别Cache层次的数据查找流程如下:首先检查目标数据块是否已存在于当前层次缓存中,在此过程中若发现对应的数据块,则无需深入查找(即完成一次命中)。如果未命中,则将转向下一个层级进行查询(即发生不命中)。同时,在发生缓存缺失时系统会触发缓存替换机制,并根据预设策略选择相应的方法进行数据更新(即触发不命中处理)。这种设计不仅保证了查询效率的同时也为系统的扩展性提供了良好的保障
7.6 hello进程fork时的内存映射
当shell进程执行分支(fork)操作时,在内核层面会生成所需的数据结构并赋予其唯一的Process ID。用于生成该新进程的虚拟内存过程中,系统会复制源进程中mm_struct区域架构以及页表的完全复制。该新进程的所有页面均设置为只读状态,并将两进程中各自的所有区域架构指定为其专用的复制方式。

图7.8 子进程与父进程的内存映射
如图所示,在图中所展示的共享对象即为子进程,在这种情况下它们之间的虚拟内存与物理内存关系如图所示。
7.7 hello进程execve时的内存映射
( 以下格式自行编排,编辑时删除 )
execve系统调用负责将当前进程重新划分,并分配给可执行文件足够的空间;随后将其参数放置在当前进程64M的末尾,并确保应用程序的顺利运行。
加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域。
(2)建立专用区域以实现对新程序的代码、data.bss以及栈的管理,在这些存储空间中安排了全新的专用空间,并将它们设定了专有属性,并且采用复制-on-write策略进行内存管理。
(3)建立共享区域映射关系,在Linux系统中,默认情况下只允许内核模块加载公共模块表文件liba.h和其他相关模块表文件,并通过这些模块表文件实现对公共模块表的引用;将hello应用程序与公共so库libc.so进行连接;随后会动态地将其连接至该应用程序中;进而将该公共模块分配至用户虚拟地址空间内的共享区域内。
初始化程序计数器寄存器。其最终操作是将当前进程的上下文程序计数器设为代码段入口地址。
7.8 缺页故障与缺页中断处理
所谓缺页现象本质上是虚拟内存访问中出现页面未命中现象。即当我们试图访问某个特定的虚拟内存地址对应的页面时,在物理内存中并不存在该页面副本的情况下就被称为缺少页面事件(missing page)。然而由于磁盘读取速度极其缓慢因此导致每次发生缺少页面事件都会产生极大的性能代价使得类似缓存机制无法简单地应用到这里必须依赖软硬件协同工作的方式才能有效解决此类问题为此系统设计了一个专门负责处理缺少页面事件的操作系统内核模块一旦发生‘缺少页面’事件系统将流程返回至操作系统核心模块并立即调用相关算法进行计算以确定应当替换成哪个页面随后将通过预先建立起来的数据结构快速确定应当替换成哪个页面并将该数据块从被占用的位置上替下来完成整个换 page 过程之后系统将流程返回至之前触发缺少页面事件的那个进程继续执行其原有的操作序列
7.9动态存储分配管理
所谓动态内存分配(Dynamic Memory Allocation)即为一种能够根据程序需求灵活管理存储空间的技术。与数组等静态内存管理方法相比动态内存管理更加灵活其核心特点在于无需预先预留固定大小的空间而是能够依据实际需求进行资源的动态获取与回收这种机制不仅提升了系统的运行效率也为现代软件开发提供了更为灵活可靠的解决方案
当程序运行至需为变量或对象动态分配内存时,必须向系统请求获取堆内存中一块所需大小的空间,并用于存储该变量或对象。当该变量或对象不再被使用时,在其生命周期结束时,则需显式地回收其所占用的存储空间以避免浪费资源。通过这种方式使得系统能够对该堆内存重新进行分配从而提高资源利用率。
寻找一个空闲块的方式有三种:
在首次适配过程中,在内存管理中系统会从空闲链表的头部开始搜索可用空间区域。为了提高资源利用率,在计算时采用总可用空间数量的线性比例作为基础参数。然而这种算法实现可能会导致在链表起始附近产生小规模的空隙或断档现象。
(2)在随后的下一次匹配中,在链表中从上次查询结束后的位置开始进行操作。这种机制具有更快的响应速度,并且通过无需重新扫描那些无用区块来提高效率。然而,一些研究结果表明,在后续匹配中的内存利用率相对较低。
(3)最优匹配:通过遍历链表节点列表,在合适的位置插入一个新的空闲块以实现最优匹配,在剩余空间最小的情况下实现最大限度地减少碎片化——能够有效提升内存使用效率,并且在初次分配时可能不会影响性能表现。然而,在长期运行中可能会导致启动时速度稍慢于首次分配。
同时,为了减小内存空间的碎片,还需要使用链表来对某些块来进行合并。
7.10本章小结
从程序中的地址表示逐步深入到硬件层次对地址的组织与表示,并详细阐述了两者之间的关联性。此外,还简要分析了操作系统与硬件在缓存、内存与磁盘空间管理方面的策略设置。进一步深入探讨了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函数作为核心的系统功能,并涵盖使用traps/interrupts和syscall指令(如int 0x80)进行操作
字符显示驱动模块:经过ASCII至字模库至显示VAM的过程(记录每个像素点的RGB值)。
显示芯片依据刷新周期依次扫描vram,并经由信号线路传递每一个像素的RGB信息。
8.4 getchar的实现分析
异步异常-键盘中断事件的处理:键盘中断处理子程序。该子程序接收来自设备的按键扫描码并将其转换为ASCII码值,并将这些值存储于底层操作系统的 keyboard buffer 中。
调用如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 shell通过fork机制创建一个子进程,并利用execve函数将目标程序加载到内存中进行执行。
在hello执行前,CPU为其分配时间片,hello按顺序执行自己的指令。
在执行hello指令时,在计算机的中央处理器(CPU)为它分配了一块虚拟内存空间,并将该虚拟地址映射到物理内存中以进行访问。
当hello访问内存时,函数向堆栈中申请动态的访问。
在运行过程中, hello可能遇到多种不同的信号和异常情况, 可能通过键盘输入操作, 也可能仅仅是在与其他程序之间切换, 也有可能是内核要求其终止处理。
最后,hello成功运行,shell父进程对其进行回收,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/进程/382503
[2] :进程.
[3] 深入理解计算机系统
[4] 百度百科:地址
[5] :将逻辑内存转换为物理内存空间多多是小坏熊的博客-
[HITICS] 哈尔滨工业大学2019秋季《计算机体系结构:并行与多处理》课程作业-程序人生——Hello’s P2P_北言栾生的博客-博客
[7]百度百科:动态内存分配
