摘 要

本文主要介绍了从源程序hello.c到可执行目标程序hello的变化过程,详细阐述了预处理、编译、汇编、链接等阶段。在每一阶段中,通过各种调试工具对该阶段所得文件的内容加以分析,并与前一阶段所得文件加以对比。同时,对hello的进程管理、存储管理和IO管理进行了系统解释与分析。对于把握计算机系统的整体框架结构大有益处。

关键词:预处理;编译;汇编;链接;进程管理;存储管理;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的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

(1)P2P(From Program to Process):

源程序(文本)为hello.c。在预处理阶段预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,得到修改了的源程序(文本)hello.i;在编译阶段编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包括一个汇编语言程序;在汇编阶段,汇编器(as)将hello.s翻译为机器语言指令,把这些指令打包为可重定位目标程序的格式,并将结果保存在目标文件hello.o中(hello.o是一个二进制文件);在链接阶段,链接器(ld)将单独的预编译好了的目标文件合并到hello.o中,结果得到hello文件;在shell中输入相关命令,调用fork函数创建新的子进程。

(2)020(From Zero-0 to Zero-0):

在shell中输入相关命令,调用fork函数创建新的子进程,通过execve函数加载并运行程序,映射到相应的虚拟内存空间,加载需要的物理内存,开始运行hello,CPU为其分配时间片等。当hello运行完毕,父进程回收hello进程,内核清除相关信息。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:x64 CPU;3.20GHz;16G RAM

软件环境:Windows 11 64位;Ubuntu 20.04

开发与调试工具:edb,gcc,vim,objdump,readelf

1.3 中间结果

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

表1 中间结果文件
文件名字文件作用
hello.ihello.c预处理后得到的文本文件
hello.shello.i编译后得到的文本文件
hello.ohello.s汇编后得到的二进制文件
hello_elf.txthello.o的ELF格式文件
hello_obj.txthello.o的反汇编文件
hellohello.o链接得到的可执行目标文件
hello_elf2.elfhello的ELF格式文件
hello_obj2.txthello的反汇编文件

1.4 本章小结

本章主要介绍了hello的P2P和020的整个过程、软硬件环境、开发工具以及相应的中间结果,有助于梳理清楚大致的脉络框架。

第2章 预处理

2.1 预处理的概念与作用

(1)预处理概念:

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。结果得到另一个C程序,通常是以.i作为文件扩展名。

(2)预处理的作用:

①处理源文件:搜索指定的文件,并将内容包含进来。比如hello.c中#include命令告诉预处理器读取系统头文件stdio.h中的内容,并把它直接插入程序文本中。

②处理条件编译:根据条件有选择性的保留或者放弃源文件中的内容。常见的条件包含#if、#ifdef、#ifndef指令开始,以#endif结束。用#undef指令可对用#define定义的标识符取消定义。

③进行宏替换:对于宏定义,将代码中所有使用宏定义的地方用符号表示的实际值替换该符号。

④进行特殊控制:如进行行控制——改变预定义宏“__LINE__”的值,如果后面的字符串存在,则改变“__FILE__”的值;发送抛错指令——在预处理期间发出一个诊断信息,停止转换;发送杂注指令——传递额外信息,实现某些控制。

⑤处理特殊符号:识别特殊符号,进行部分替换。

⑥将程序中的所有注释删去。

2.2在Ubuntu下预处理的命令

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

图2.2-1 预处理命令截图
图2.2-2 hello.i内容部分截图

2.3 Hello的预处理结果解析

hello.c只有24行,而经过预处理得到的hello.i有3061行。文件开头是预处理器生成的一些相关信息,之后是对头文件stdio.h、unistd.h、stdlib.h中的内容展开,最后是删去所有注释的源代码部分。同时,除了以上3个文件外,hello.i中还出现了其他文件,说明这3个文件内容中也包括了其他头文件。

图2.3-1 预处理器生成的相关信息截图
图2.3-2 包含的库文件截图
图2.3-3 源代码部分截图

2.4 本章小结

本章主要介绍了预处理的概念与作用,以及在Ubuntu下实现了预处理过程,并对预处理得到的hello.i文件进行分析。

第3章 编译

3.1 编译的概念与作用

(1)编译的概念:

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。

(2)编译的作用:

①词法分析:对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序;

②语法分析:以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构;

③语义检查和中间代码生成:使编译程序的结构在逻辑上更为简单明确,特别是使目标代码的优化比较容易实现中间代码;

④代码优化:对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码;

⑤目标代码生成:把语法分析后或优化后的中间代码变换成目标代码。

3.2 在Ubuntu下编译的命令

编译命令:gcc -S hello.i -o hello.s

图3.2-1编译命令截图
图3.2-2 hello.s内容部分截图

3.3 Hello的编译结果解析

3.3.1数据

①常量以立即数的形式出现

源代码if(argc!=4)中的4:

图3.3.1-1 常量例1截图

源代码exit(1)中的1:

图3.3.1-2 常量例2截图

源代码for(i=0;i<8;i++)中i<8被优化为i<=7,立即数7:

图3.3.1-3 常量例3截图

②局部变量

源代码中的i:

初始值为0:

图3.3.1-4 i初始值截图

每次循环i=i+1,并与7进行比较:

图3.3.1-5 i循环截图

源代码中的argc:

与4比较:

图3.3.1-6 argc与4比较截图

③表达式

源代码argc!=4,argc与4比较:

图3.3.1-7 表达式例1截图

源代码i=0,赋初值:

图3.3.1-8 表达式例2截图

源代码i++,在每次循环+1:

图3.3.1-9 表达式例3截图

源代码i<8,由于已被优化为7,所以与7进行比较:

图3.3.1-10 表达式例4截图

3.3.2赋值

源代码i=0,为i赋初值,通过movl指令实现:

图3.3.2-1 赋值例1截图

源代码i++,在每次循环i赋值为i+1,通过addl指令实现:

图3.3.2-2 赋值例2截图

3.3.3算术操作

源代码i++,通过addl指令实现:

图3.3.3-1 算术操作截图

3.3.4关系操作

源代码argc!=4,将argc与4比较,通过cmpl指令实现:

图3.3.4-1 关系操作例1截图

源代码i<8,将i与8比较,由于i<8已被优化为i<=7,所以实际将i与7比较,通过cmpl指令实现:

图3.3.4-2 关系操作例2截图

3.3.5数组操作

源代码printf(“Hello %s %s\n”,argv[1],argv[2])。先在%rax中存储-32(%rbp),再将%rax加上16,最后将%rax指向的数据传递给%rdx;先在%rax中存储-32(%rbp),再将%rax加上8,将%rax指向的数据存储在%rax中,将%rax的值传递给%rsi;也就是argv[1]和argv[2]的值存储在%rsi和%rdx中,最后调用printf函数:

图3.3.5-1 数组操作例1截图

源代码sleep(atoi(argv[3])),将argv[3]存储在%rdi中:

图3.3.5-2 数组操作例2截图

3.3.6控制转移

①if语句:

源代码if(argc!=4){

printf(“用法: Hello 学号 姓名 秒数!\n”);

exit(1);

}

通过cmpl指令和je指令,将agrc与4作比较,如果argc=4则跳转至.L2部分执行,否则继续执行下面的语句,直到最后退出:

图3.3.6-1 if语句截图

②for循环:

源代码for(i=0;i<8;i++){

printf(“Hello %s %s\n”,argv[1],argv[2]);

sleep(atoi(argv[3]));

}

循环中的内容和+1操作在.L3部分执行,判断在.L4部分执行,通过cmpl指令和jle指令,如果i<=7,则跳转到.L4部分继续执行:

图3.3.6-2 for循环截图

3.3.7函数操作

①main函数:

传入参数argc和*argv[],返回0:

图3.3.7-1 main函数截图

②printf函数:

源代码printf(“用法: Hello 学号 姓名 秒数!\n”)调用时传入字符串的首地址,通过call指令转移到指定程序:

图3.3.7-2 printf函数例1截图

源代码printf(“Hello %s %s\n”,argv[1],argv[2])调用时传入参数argv[1]和argv[2],通过call指令转移到指定程序:

图3.3.7-3 printf函数例2截图

③exit函数:

通过%edi传递参数1,通过call指令转移到指定位置:

图3.3.7-4 exit函数截图

④sleep函数:

通过%rdi传递参数,通过call指令转移到指定位置:

图3.3.7-5 sleep函数截图

3.4 本章小结

本章主要介绍了编译的概念与作用,在Ubuntu下编译的命令,同时对hello的编译结果进行解析,详细分析了编译器如何处理C语言中的数据、赋值、算术运算、关系操作、数组操作、控制转移和函数操作。有助于理解C语言在汇编语言中的表现形式。

第4章 汇编

4.1 汇编的概念与作用

(1)汇编的概念:

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

(2)汇编的作用;

将汇编代码转换为二进制文件,也就是机器语言指令,并将指令打包。汇编指令包括数据传输指令、算术和逻辑指令、分支和循环指令、过程调用指令、处理器控制指令,通过这些指令完成汇编的过程。

4.2 在Ubuntu下汇编的命令

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

图4.2-1 汇编命令截图

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

4.3.1readelf命令

命令:readelf -a helo.o > hello_elf.txt(导出elf文件至hello_elf.txt)

图4.3.1 readelf命令截图

4.3.2ELF头

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

图4.3.2-1 ELF头截图

4.3.3节头

不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

图4.3.3-1 节头截图

4.3.4重定位节

重定位节中包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对这些变量符号进行修改。链接的时候链接器会根据重定位节的信息对外部变量符号决定选择何种方式计算正确的地址,通过偏移量等信息计算出正确的地址。

图4.3.4-1 重定位节截图

.rodata中的内容、puts、exit、printf、atoi、sleep、getchar都需要通过重定位计算正确的地址。且只出现了R_X86_64_PC_32和R_X86_64_PLT32两种重定位类型。

4.3.5符号表

.symtab存放在程序中定义和引用的函数和全局变量的信息。

图4.3.5-1 符号表截图

main、puts、exit、printf、atoi、sleep、getchar等函数都在.symtab中出现。

4.4 Hello.o的结果解析

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

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.4.1objdump命令

命令:objdump -d -r hello.o > hello_obj.txt(将反汇编文件导出至hello_obj.txt)

图4.4.1-1 objdump命令截图
图4.4.1-2 反汇编代码截图

4.4.2机器语言与汇编语言对比

①操作数:

hello.s中的操作数是十进制数,如下图中的$16;而hello.o反汇编中的操作数是十六进制数,如下图中的$0x10。

图4.4.2-1 hello.o的反汇编中操作数截图
图4.4.2-2 hello.s中操作数截图

②分支转移:

hello.s中跳转时显示的是段的名字,如下图中的.L2;而hello.o由于已经进行重定位,因此跳转时显示的是跳转的目的地址(与main函数的相对偏移地址),如下图中的2f。

图4.4.2-3 hello.o的反汇编中分支转移截图
图4.4.2-4 hello.s中分支转移截图

③函数调用:

hello.s中call指令后显示的是函数名称,如下图的exit;而hello.o的反汇编中call指令后显示的是与main函数的相对偏移地址,如下图的2f。

图4.4.2-5 hello.o的反汇编中函数调用截图
图4.4.2-6 hello.s中函数调用截图

4.5 本章小结

本章主要介绍了汇编的概念与作用,在Ubuntu下汇编的命令,对hello.o的ELF格式进行分析,包括ELF头、节头、重定位节、符号表等内容,对hello.o的结果进行解析,对比分析了hello.o反汇编的结果与hello.s的区别,着重分析了操作数、分支转移、函数调用的区别。有助于理解ELF格式以及汇编语言和机器语言的区别。

第5章 链接

5.1 链接的概念与作用

(1)链接的概念:

链接是指将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。

(2)链接的作用:

使得分离编译成为可能。不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

5.2 在Ubuntu下链接的命令

链接命令:ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello

图5.2-1 链接命令截图

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.3.1readelf命令

命令:readelf -a hello >hello_elf2.elf

图5.3.1-1 readelf命令截图

5.3.2ELF头

内容与4.3.2中介绍类似。

图5.3.2-1 ELF头截图

5.3.3节头

描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并为一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

图5.3.3-1 节头截图

5.3.4重定位节

重定位节内容变为需要动态链接调用的函数,同时重定位类型发生改变。

5.3.4-1 重定位节截图

5.3.5符号表

存放在程序中定义和引用的函数和全局变量的信息,连接后符号表条目增加。

图5.3.5-1 符号表截图

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

  • 使用edb加载hello。
图5.4-1 edb加载hello截图

从Data Dump窗口可以看到hello的虚拟地址空间分配情况。

图5.4-2 Data Dump窗口截图

②.init段起始地址:0x401000

图5.4-3 .init段起始地址截图

③.text段起始地址:0x4010f0

图5.4-4 .text段起始地址截图

以此类推,节头中的各段在edb中均能对应找到,说明节头表中存储各段的起始地址与各段的虚拟地址之间存在对应关系。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

5.5.1objdump命令

命令:objdump -d -r hello > hello_obj2.txt(将文件导出至hello_obj2.txt)

图5.5.1-1 objdump命令截图

5.5.2hello与hello.o对比及重定位过程

①hello比hello.o增加了一些节,例如.init、.plt、.plt.sec等。

图5.5.2-1 .init截图
图5.5.2-2 .plt截图
图5.5.2-3 .plt.sec截图

②hello比hello中增加了一些函数,例如_init等。

图5.5.2-4 _init函数截图

③hello删去了hello.o中的重定位条目,hello.o中跳转的目的地址和函数地址都是与main函数的相对偏移地址,hello中跳转的目的地址和函数地址都是虚拟内存地址。

图5.5.3-4 hello.o中跳转截图
图5.5.3-5 hello中跳转截图

5.5.3链接过程

主要分两步:

①符号解析:目标文件定义和引用符号,每个符号对应于一个函数、一个局部变量或一个静态变量。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。

②重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

5.6.1执行过程

使用edb执行hello截图:

图5.6.1-1 edb执行hello截图

①从加载hello到_start:

程序先调用_init函数,之后是puts、printf等库函数,最后调用_start函数。

②从_start到call main:

程序先调用__libc_csu_init等函数,完成初始化工作,随后调用main函数。

③从main函数到程序终止:

程序执行main函数调用main函数用到的一些函数,main函数执行完毕之后调用__libc_csu_fini、_fini完成资源释放和清理的工作。

5.6.2子程序名或程序地址

0000000000401000

0000000000401020

0000000000401090

00000000004010a0

00000000004010b0

00000000004010c0

00000000004010d0

00000000004010e0

00000000004010f0

0000000000401120

0000000000401125

00000000004011c0

0000000000401230

0000000000401238

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

程序调用由共享库定义的函数,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。GNU编译系统使用延迟绑定将过程地址的绑定过程推迟到第一次调用该过程时。通过两个数据结构——GOT和PLT协作在运行时解析函数的地址。

.got.plt的首地址为0x404000,通过edb观察其在_init前后的内容变化。

图5.7-1 .got.plt在_init前的内容截图
图5.7-2 .got.plt在_init后的内容截图

5.8 本章小结

本章主要介绍了链接的概念与作用,在Ubuntu下链接的命令,可执行目标文件hello的格式,包括ELF头、节头、重定位节、符号表,通过对比hello与hello.o进行链接的过程分析和重定位过程分析,对hello的执行流程和动态链接进行分析。

第6章 hello进程管理

6.1 进程的概念与作用

(1)进程的概念:

进程的经典定义就是一个执行中程序的实例。

(2)进程的作用:

每次用户向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文运行它们自己的代码或其他应用程序。进程提供给应用程序两个关键抽象:

①一个独立的逻辑流,它提供一个假象,好像我们的程序独占地使用处理器。

②一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

(1)壳shell-bash的作用:

bash是变种、缺省的Linux shell,是信号处理的代表,负责各进程创建与程序加载运行及前后台控制、作业调用、信号发送与管理等;是一个交互型应用级程序,代表用户运行其他程序;执行一系列的读/求值等步骤;读步骤读取用户的命令行,求值步骤解析命令,代表用户运行。

(2)壳shell-bash的处理流程:

①判断命令是否通过绝对路径执行;
②判断命令是否存在alias别名;
③判断用户输入的是内部命令还是外部命令;
④Bash内部命令直接执行,外部命令检测是否存在缓存;
⑤通过PATH路径查找命令,有执行,无报错;

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程;子进程返回0,父进程返回子进程的PID;新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(代码、数据段、堆、共享库以及用户栈);子进程获得与父进程任何打开文件描述符相同的副本,子进程有不同于父进程的PID;fork函数:被调用一次,却返回两次。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件,且带参数列表和环境变量列表。只有当出现错误时,execve才会返回到调用程序。所以,与fork依次调用返回两次不同,execve调用一次并从不返回。在execve加载了可执行目标文件之后,其调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

(1)进程上下文信息:

系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

(2)进程时间片:

一个进程执行它的控制流的每一部分的每一时间段叫做时间片。

(3)进程调度的过程:

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中被称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,他就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换①保存当前进程的上下文;②恢复某个先前进程被抢占的进程被保存的上下文;③将控制传递给这个新恢复的进程。

(4)用户态与核心态转换:

运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.6.1异常

①中断(异步异常):处理器外部I/O设备引起;

②陷阱(同步异常):有意的,执行指令的结果;

③故障(同步异常):不是有意的,但可能被修复;

④终止(同步异常):非故意,不可恢复的致命错误造成。

6.6.2信号

①中断:信号SIGTSTP,默认行为是停止直到下一个SIGCONT;

②终止:信号SIGINT,默认行为是终止。

6.6.3处理方式

中断:

图6.6.3-1 中断处理方式

陷阱:

图6.6.3-2 陷阱处理方式

故障:

图6.6.3-3 故障处理方式

终止:

图6.6.3-4 终止处理方式

6.6.4命令、运行结果、异常与信号的处理

①正常运行:程序每隔3秒输出一次,共输出8次。

图6.6.4-1 正常运行结果截图

②不停乱按:将屏幕输入缓存到缓冲区,不影响当前进程的执行。

图6.6.4-2 不停乱按结果截图

③回车:不影响当前进程的执行。

图6.6.4-3 回车结果截图

④Ctrl-Z后运行ps命令:Ctrl-Z挂起前台作业,ps显示进程的详细信息。

图6.6.4-4 Ctrl-Z后运行ps命令截图

⑤Ctrl-Z后运行jobs命令:Ctrl-Z挂起前台作业,jobs显示任务列表和任务状态。

图6.6.4-5 Ctrl-Z后运行jobs命令截图

⑥Ctrl-Z后运行pstree命令:Ctrl-Z挂起前台作业,pstree以树状结构显示进程之间的关系。

图6.6.4-6 Ctrl-Z后运行pstree命令1
图6.6.4-7 Ctrl-Z后运行pstree命令2

⑦Ctrl-Z后运行fg命令:Ctrl-Z挂起前台作业,fg %n使第n个任务在前台运行,此处是第一个任务。

图6.6.4-8 Ctrl-Z后运行fg命令截图

⑧Ctrl-Z后运行kill命令:Ctrl-Z挂起前台作业,kill杀死进程。通过kill -9 8993给进程8993发送SIGKILL信号。

图6.6.4-9 Ctrl-Z后运行kill命令截图

⑨Ctrl-C:发送SIGINT信号,结束hello。在ps中没有其相关信息。

图6.6.4-10 Ctrl-C结果截图

6.7本章小结

本章主要介绍了进程的概念与作用、壳Shell-bash的作用与处理流程、fork进程创建过程、execve过程、进程执行和异常与信号处理等内容。有助于通过hello理解进程、异常、信号等概念以及其在实践中的体现与作用。

第7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

(1)逻辑地址:

由程序产生的与段相关的偏移地址部分。如hello.o中跳转的目的地址和函数地址都是与main函数的相对偏移地址。

(2)线性地址:

段地址+偏移地址=线性地址,线性地址是逻辑地址到物理地址变换的中间结果。如hello中代码与数据的地址。

(3)虚拟地址:

虚拟内存是对整个内存的抽象描述,是相对于物理内存来讲的。虚拟内存也就是线性地址空间。如hello中的地址。

(4)物理地址:

用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。在hello运行中,需要根据虚拟地址通过地址翻译得到物理地址,并通过物理地址访问其在内存中的位置。

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

(1)段寄存器:

图7.2-1 段寄存器的含义

段寄存器(16位)用于存放段选择符

CS(代码段):程序代码所在段;

SS(栈段):栈区所在段;

DS(数据段):全局静态数据区所在段;

其他3个段寄存器ES、GS和FS可指向任意数据段;

(2)段选择符:

图7.2-2 段选择符

①TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT);

②CS寄存器中的RPL字段表示CPU的当前特权级。RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,第0级高于第3级;

③高13位-8K个索引用来确定当前使用的段描述符在描述符表中的位置。

(3)段描述符:

段描述符是一种数据结构,实际上就是段表项,分两类:用户的代码段和数据段描述符、系统控制段描述符,其中系统控制段描述符又分两种:特殊系统控制段描述符,包括:局部描述符表(LDT)描述符和任务状态段(TSS)描述符;控制转移类描述符,包括:调用门描述符、任务门描述符、中断门描述符和陷阱门描述符。

(4)描述符表:

描述符表实际上就是段表,由段描述符(段表项)组成。有三种类型:

①全局描述符表GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及TSS(任务状态段)等都属于GDT中描述的段;

②局部描述符表LDT:存放某任务(即用户进程)专用的描述符;

③中断描述符表IDT:包含256个中断门、陷阱门和任务门描述符。

(5)Intel处理器的存储器寻址:

图7.2-3 Intel处理器的存储器寻址

(6)段式管理:

根据段选择符定位到相应的段描述符,根据段描述符在描述符表中得到相应的段基址,加上偏移量,得到线性地址。

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

(1)页表:

页表是一个页表条目的数组,将虚拟页地址映射到物理页地址。

(2)页命中:

虚拟内存中的一个字存在于物理内存中,即(DRAM缓存命中)。
(3)缺页及缺页处理:

虚拟内存中的字不在物理内存中(DRAM缓存不命中)。缺页导致页面出错 (缺页异常);缺页异常处理程序选择一个牺牲页;导致缺页的指令重新启动: 页面命中。

(4)分配一个新的虚拟页:

内核在磁盘上分配,并将页表指向这个位置。

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

若TLB命中,则MMU从TLB中取出相应的PTE,将这个虚拟地址翻译为物理地址;若TLB不命中,根据VPN1在一级页表选择对应的PTE,该PTE包含二级页表的基地址;根据VPN2在二级页表选择对应的PTE,该PTE包含三级页表的基地址;根据VPN3在三级页表选择对应的PTE,该PTE包含四级页表的基地址;在四级页表取出对应的PPN,与VPO串联起来,就得到相应的物理地址。

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

首先访问一级Cache,寻找该物理内存对应的内容是否已被缓存且有效,若已被缓存且有效,则缓存命中;否则缓存不命中,则需要访问二级Cache,重复上述步骤;若二级Cache中依然缓存不命中,则需要访问三级Cache,直到访问主存。将访问到的内容分别加载进上一层缓存,再进行后续操作。

7.6 hello进程fork时的内存映射

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

当fork函数在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

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

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

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

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

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

DRAM缓存不命中称为缺页。CPU引用了VPm中的一个字,VPm并未缓存在DRAM中。地址翻译硬件从内存中读取PTEm,从有效位推断出PTEm未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页(假设其为VPn)。如果VPn已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VPn的页表条目,反映出VPn已经不在缓存在主存中这一事实。

接下来,内核从磁盘复制VPm到内存中,更新PTEm,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接着未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

分配器将堆视为一组大小不同的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。

①显式分配器,要求应用显式地释放任何已分配的块。

②隐式分配器,另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程就叫做垃圾收集。

7.10本章小结

本章主要介绍了存储器地址空间、Intel逻辑地址到线性地址的变换-段式管理、线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到VP的变换、三级Cache支持下的物理内存访问,进程fork和execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。有助于理解程序的存储管理的各种机制,以及有关的存储方式与管理策略。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对对应文件的读和写来执行。

设备管理:unix io接口。将设备映射为文件的方式,使得Linux内核引出一个简单、低级的应用接口,称为I/O接口,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

8.2.1Unix IO接口

Unix IO使得所有的输入和输出都能以一种统一且一致的方式来执行:

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

②Linux Shell创建的每个进程开始时都有三个打开的对应文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。

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

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

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

8.2.2Unix IO函数

①打开文件:int open(char *filename, int flag, mode_t mode);

进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,也可以是一个或者更多位掩码的或,为写提供给一些额外的指示。mode参数指定了新文件的访问权限位。

②关闭文件:int close(int fd);

进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。

③读文件:ssize_t read(int fd, void *buf, size_t n);

应用程序通过调用read函数执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

④写文件:ssize_t write(int fd, const void *buf, size_t n);

应用程序通过调用write函数执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

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

printf函数调用vsprintf函数进行格式化,接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出;调用write函数把buf中的i个元素的值写到终端。syscall函数不断地打印出字符,直到遇到:’\0’。

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar函数体:

int getchar(void){ static char buf[BUFSIZE]; static char *b=buf; static int n=0; if(n==0) { read(0,buf,BUFSIZE); b=buf; } return ((--n)>0) " />用户从键盘输入,键盘接口得到对应的键盘扫描码,同时发送中断请求,通过键盘中断处理子程序,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar的返回值是用户输入字符的ascii码,若到文件结尾则返回-1(EOF),且将用户输入显示到屏幕。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,同时对printf和getchar的实现进行分析。通过对熟悉函数的分析深入了解函数实现与IO的关系,有助于理解IO管理的概念和相关函数。

结论

(1)用计算机系统的语言,逐条总结hello所经历的过程。

本文围绕hello所经历的过程展开,其中hello所经历的重要结点包括:

①编写源程序(文本)hello.c;

②hello.c经过预处理器(cpp)的预处理得到修改了的源程序(文本)hello.i;

③hello.i经过编译器(ccl)的编译得到汇编程序(文本)hello.s;

④hello.s经过汇编器(as)的汇编得到可重定位目标程序(二进制)hello.o;

⑤hello.o经过链接器(ld)将其与其它目标文件合并得到可执行目标文件hello;

⑥shell调用fork函数创建子进程;

⑦shell调用execve函数加载hello程序,映射到对应的虚拟内存;

⑧hello程序执行过程中通过进程管理实现异常与信号的处理,存储管理实现内存访问,同时相应的IO设备配合hello程序实现输入输出等功能;

⑨程序结束,父进程对其进行回收,内核将其从系统中清除。

(2)你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

计算机系统的设计与实现首先需要深刻理解计算机系统的各种概念,包括运行机制、原理等,了解程序执行过程中的基本流程,包括预处理、编译、汇编、链接等阶段以及相应的细节。在此基础上,通过对进程管理、存储管理、IO管理等加深对整体框架结构的认识。同时,依据计算机系统的相关原理,可以在安全性、高效性等方面对程序做进一步优化;针对各种可能存在的安全风险进行有效防范。因此,熟练掌握计算机系统相关知识无论是对于加深对计算机的理解,还是编写更加优秀的程序都至关重要。

附件

列出所有的中间产物的文件名,并予以说明起作用。

表2 中间结果文件
文件名作用
hello.ihello.c预处理后得到的文本文件
hello.shello.i编译后得到的文本文件
hello.ohello.s汇编后得到的二进制文件
hello_elf.txthello.o的ELF格式文件
hello_obj.txthello.o的反汇编文件
hellohello.o链接得到的可执行目标文件
hello_elf2.elfhello的ELF格式文件
hello_obj2.txthello的反汇编文件

参考文献

[1] 程序预处理阶段,在做什么_预处理阶段主要做的是哪两件事-CSDN博客

[2] 程序详细编译过程(预处理、编译、汇编、链接) - 知乎

[3] 编译_百度百科

[4] https://wenku.csdn.net/answer/3865203b590443c0b62e8743d9038bd1

[5] Linux(四):什么是Bash、什么是shell?_bash shell-CSDN博客

[6] Linux Bash shell - 知乎

[7] 逻辑地址、物理地址、虚拟地址_虚拟地址 逻辑地址-CSDN博客

[8] https://www.cnblogs.com/pianist/p/3315801.html

[9]《深入理解计算机系统》 Randal E.Bryant & David R.O’Hallaron 机械工业出版社