CSAPP大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业   未来技术学院          

学     号    2022113158           

班     级    22WL022              

学       生    郑玺宝               

指 导 教 师    刘宏伟                 

计算机科学与技术学院

2024年5月

摘  要

本文从Hello程序出发,深入剖析了一个程序由高级C语言代码经过预处理、编译、汇编以及链接等多个阶段,最终转化为可执行文件的完整生命周期。通过以上流程,计算机系统将硬件与系统软件精密配合,层层深入,共同实现了应用程序的运行。通过一系列分析,将书中所学知识融会贯通,进而加深对计算机系统的理解。

关键词:计算机系统  程序  进程  P2P  编译  链接                         

目  录

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

1.1.1 P2P

P2P (From Program to Process):从程序到进程,Hello的P2P过程如下:

  • 编辑hello.c文本。
  • GCC编译器驱动程序完成从源文件到目标文件的转化:
  1. 预处理:cpp对原始c程序进行修改,得到hello.i;
  2. 编译:ccl将hello.i翻译成文本文件hello.s(包含一个汇编语言程序,每条语句都以一种文本格式描述了一条低级机器语言指令);
  3. 汇编:as将hello.s翻译成机器语言指令,将其打包成可重定位目标程序,并将结果保存在二进制文件hello.o中;

d. 链接:ld将hello.o与printf.o合并,得到可执行目标文件Hello。

③ 在shell中启动,调用fork函数创建新进程,在新进程中调用execve函数加载并运行Hello,通过内存映射,分配空间等让Hello与其他进程并发进行。

       经过以上步骤,hello就完成了从程序到进程的变化。

1.1.2 020

020 (From Zero to Zero):从0到0。源程序从无到有被编写,所以是从0开始,然后编译生成可执行目标文件Hello,而后在运行的时候拥有了自己的进程,也在内存中存储了相关信息,进程终止之后被回收并释放,所以是以0结束。

1.2 环境与工具

1.2.1 硬件环境

       X64 CPU;2.30GHz;16.0G RAM ;Windows11 64位;Ubuntu 22.04

1.2.2 软件环境

       Vmware 17.5.1;Visual Studio 2022

1.2.3 开发工具

       CodeBlocks 64位;vi/vim/gedit+gcc

1.3 中间结果

图1 编译系统(摘自《深入理解计算机系统》)

表1 中间结果文件

文件名称

文件说明

hello.i

预处理生成的文本文件

hello.s

.i文件编译后得到的汇编语言文件

hello.o

.s文件汇编后得到的可重定位目标文件

hello

.o经过链接生成的可执行目标文件

1.4 本章小结

本章分析了P2P和020的过程,介绍了为编写本论文使用的软硬件环境和开发与调试工具,并列出了本实验的中间结果文件及其说明。

第2章 预处理

2.1 预处理的概念与作用

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。如hello.c中第一行的#include <stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果得到了另一个C程序,通常以.i作为文件扩展名。

2.2在Ubuntu下预处理的命令

在Linux终端输入命令:cpp hello.c hello.i,得到如图所示的预处理后的文件hello.i:

图2 预处理

2.3 Hello的预处理结果解析

预处理后的hello.i从hello.c的24行增加到3092行,同时发现main函数在文件的最后部分。预处理对头文件和宏定义进行了操作,所有的#include等语句全部被替换,取而代之的是一些路径以及用到的相关语句。并且预处理同时也将所有的注释信息删除了。

2.4 本章小结

本章介绍了预处理的概念、作用以及实现方式,查看并分析了hello.c在Ubuntu下预处理的结果。

第3章 编译

3.1 编译的概念与作用

编译器将后缀为.i的文本文件翻译成后缀为.s的文本文件,它包含一个汇编语言程序,其中的每条语句都以一种文本格式描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。

3.2 在Ubuntu下编译的命令

在Linux终端输入命令:gcc -S hello.c -o hello.s,得到如图所示的编译后的文件hello.s:

图3 编译

3.3 Hello的编译结果解析

3.3.1 数据操作

      

图3 .LC0和.LC1

       LC0对应一个包含 UTF-8 编码中文字符串的局部符号;LC1对应英文的局部符号,其中的三个%s对应输入的前三个参数,即学号,姓名和手机号码。

4 .L2.L3(循环变量)

循环变量i储存在-4%rbp)处,并被赋初值为0;循环的条件为i 9

5 %rdi%rsi

       由上图最后两行可知,第一个参数argc被存储在-20%rbp)中,第二个参数指针数组argv被存储在-32%rbp)中。

3.3.2 赋值

    由图4可知,循环变量i被赋值为0

3.3.3 类型转换

图6 调用atoi函数

    如图6所示,.L4的倒数第四行调用atoi函数,将输入的字符串转换为整形后作为倒数第二行调用sleep函数的输入。

3.3.4 算术操作

    如图6所示,其中.L4的正数第二、五行,addq $24,%rax和addq $16,%rax对%rax做加法实现数组相对寻址;最后一行addl $1,-4(%rbp)每次循环对循环变量i做+1操作。

3.3.5 关系操作

图7 关系操作1

       地址-20(%rbp)处存储的是argc的值,将argc的值与5进行比较,如果相等则跳转到L2继续执行。

图8 关系操作2

       地址-4(%rbp)处存储的是i,将i的值与9进行比较,若比较结果为小于等于则跳转到L4,继续执行循环体,若大于则跳出循环。

3.3.6 数组/指针操作

    由3.3.1节可知,数组argv被存储在-32(%rbp)中。

图9 指针访问数组

       通过指针访问argv[1], argv[2]和argv[3]的存储地址,将字符串的值存储在寄存器中,作为参数传递给函数printf和atoi。

3.3.7 控制转移

  1. if语句

3.3.1节可知,参数argc被存储在-20%rbp)中。判断argc是否等于5,若相等则跳转到.L2,执行后续操作,若不等则继续执行下一条语句。

图10 if语句

  1. for循环语句

将变量i赋初值0后跳转到L3,将i的值与9进行比较,如果i小于等于9则执行循环体,单次循环结束后i++,重复上述操作直到i大于9跳出循环。

图11 for循环语句

3.3.8 函数操作

  1. main函数

传参为argc和argv,由3.1.1节可知,分别存储在-20(%rbp)和-32(%rbp)中。返回值存储在%eax中。

  1. puts函数

取出.LC0(%rip)中的字符串存入%rax中,再存入%rdi,将%rdi作为参数传递给puts。

图12 puts函数

  1. exit函数

将1赋值给%edi,作为参数传递给exit。(exit(1))

图13 exit函数

  1. printf函数

将argv[1],argv[2]和argv[3]的值取出,存储在%rsi,%rdx和%rcx中,取出.LC1(%rip)处的字符串存入%rdi中,作为参数传递给printf。

图14 printf函数

  1. atoi函数

通过指针将argv[4]的值存入%rax中,再将%rax赋给%rdi,参数通过%rdi进

传递,返回值存储在%eax中。

图15 atoi函数

  1. sleep函数

将%eax中存储的atoi函数的返回值赋给%edi,参数通过%edi进行传递,通过call指令调用。

图16 sleep函数

  1. getchar函数

通过call指令直接调用,无需传参。

图17 getchar函数

3.4 本章小结

本章介绍了编译的概念和作用,并结合hello.i的编译结果hello.s对汇编语言中的各部分以及各种操作进行了详细的说明。在编译完成的汇编语言程序中,我们可以清晰地观察到汇编代码对内存和寄存器进行的各种底层操作,进而对汇编语言和编译器工作原理有更深入的理解。

第4章 汇编

4.1 汇编的概念与作用

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

4.2 在Ubuntu下汇编的命令

在Linux终端输入命令:gcc hello.s -c -o hello.o,得到如图所示的汇编后的文件hello.o:

图18 汇编

4.3 可重定位目标elf格式

       通过命令:readelf -a hello.o > ./elf.txt导出elf.txt文件。

图19 导出elf文件

图20 典型的ELF可重定位目标文件

(摘自《深入理解计算机系统》)

4.3.1 ELF头

图21 ELF头

ELF头以一个16字节的序列开始,这个序列是对于声称该文件系统下一些字的大小等信息的描述。而后包含一些能够帮助链接器语法分析并解释目标文件的信息,包括ELF头的大小、目标文件的版本、机器类型、节头部表的文件偏移、以及节头部表中条目的大小和数量。

4.3.2 节头表

图22 节头表

节头表描述了.o文件中每一个节出现的位置、大小,目标文件中的每一个节都有一个固定大小的条目。

4.3.3 重定位节

图23 重定位节

重定位节包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对于某些变量符号进行修改。链接的时候链接器会根据重定位节的信息对于外部变量符号决定选择何种方法计算正确的地址,例如通过偏移量等信息计算。本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,atoi, sleep,getchar。

4.3.4 符号表

图24 符号表

symtab是一个符号表,它存放于程序中定义和引用的函数和全局变量的信息。例如本程序中的getchar、puts、exit等函数。每个可重定位目标模块 m 都有一个符号表,它包含 m 定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

由模块 m 定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的 C 函数和全局变量。

由其他模块定义并被模块 m 引用的全局符号。这些符号称为外部符号,对应于在其他模块中定义的非静态 C 函数和全局变量。

只被模块 m 定义和引用的局部符号。它们对应于带 static 属性的 C 函数和全局变量。这些符号在模块 m 中任何位置都可见,但是不能被其他模块引用。

READELF用一个整数索引来标识每个节,Ndx=1标识.text节,Nex=UND(UNDEF)则表示未定义的符号,即在本目标模块中引用但是在其他模块中定义的符号。

4.4 Hello.o的结果解析

在Linux终端输入命令: objdump -d hello.o > asm查看hello.o的反汇编,保存在asm中。

图25  hello.s与hello.o的反汇编结果对照

  • 分支转移:反汇编代码跳转指令的操作数由段名称变成了确定的地址。(红色框出)
  •  函数调用:在hello.s中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址。(橙色框出)
  •  数的表示:hello.s里的数是十进制表示,asm_hello.s里的数是十六进制表示。

全局变量访问:hello.s中直接通过段名称+%rip访问rodata,但是在asm_hello.s中,由于不知道rodata的数据地址,所以只能先写成0+%rip进行访问,再在后续的操作中利用重定位和链接来访问rodata。(黄色框出)

4.5 本章小结

本章介绍了hello从hello.s到hello.o的汇编过程,比较了前后的的结果,了解了从汇编语言映射到机器语言汇编器需要实现的转换。

5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时、加载时或运行时。

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

5.2 在Ubuntu下链接的命令

在Linux终端输入命令:ld -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 /usr/lib/gcc/x86_64-linux-gnu/11/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

图26 链接

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

5.3.1 elf头

helloelf的elf头和elf.txt的elf头所包含的信息种类基本相同,不同的是程序头大小和节头数量都得到了增加,并且它还包括程序的入口点(entry point),也就是当程序运行时要执行的第一条指令的地址。

图27 elf头

5.3.2 节头表

相较于elf.txt,内容更加丰富详细。当完成链接之后程序中的一些文件就被添加进来了,每一节都有了实际地址。

                                                                     图28 节头表

5.3.3 重定位节

helloelf的重定位节与elf.txt的重定位节的名字以及内容都不同,所有加数都为0,即在链接环节完成了重定位效果。

图29 重定位节

5.3.4 符号表

图30 符号表

与elf.txt相比,符号表的功能没有发生变化,所有重定位需要引用的符号都在其中说明,但是 main函数以外的符号也拥有了type,即完成了链接。

5.3.5 程序头

图31 程序头

程序头是一个结构数组,描述了系统准备程序执行所需的段或者其他信息。ELF 可执行文件被设计得很容易加载到内存,可执行文件的连续的片(chunk)被映射到连续的内存段。程序头部表(program header table)描述了这种映射关系。从程序头中可以看到根据可执行目标文件的内容初始化为了两个内存段,分别为只读内存段(代码段)和读写代码段(数据段)。

5.4 hello的虚拟地址空间

图32 虚拟地址空间

用edb打开hello,由data dump部分可以看出,程序是从0x400000开始加载的。查看.elf 中的程序头部分。程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息,各表项功能如下:

PHDR:程序头表

INTERP:程序执行前需要调用的解释器

LOAD:保存常量数据、程序目标代码等

DYNAMIC:保存动态链接器使用信息

NOTE:保存辅助信息

GNU_STACK:异常标记

GNU_RELRO:保存重定位后只读区域的位置

5.5 链接的重定位过程分析

在Linux终端输入命令:objdump -d -r hello

图33 hello反汇编

当链接器完成了符号解析后,就把代码中的每个符号引用和一个符号定义关联起来。此时,链接器可以得出输入目标模块中的代码节和数据节的确切大小。接着链接器将开始进行重定位,重定位由以下两步组成:

  • 重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的. data 节被全部合并成一个节,这个节成为输出的可执行目标文件的. data 节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。完成这一步后程序中的每条指令和全局变量都将获得唯一的运行时内存地址。
  • 重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使它们指向正确的运行时地址。链接器依赖于可重定位目标模块中称为重定位条目(relocation entry)的数据结构执行这一步。

可以看到这次的汇编代码中call时的地址都变为了绝对地址,不再是最初的函数名字或相对地址。而且多了很多通过链接加进来的函数的源代码,如printf等。

5.6 hello的执行流程

表2 hello的执行流程

名称

地址

_init

0x401000

.plt

0x401020

puts@plt

0x401090

printf@plt

0x4010a0

getchar@plt

0x4010b0

atoi@plt

0x4010c0

exit@plt

0x4010d0

sleep@plt

0x4010e0

_start

0x4010f0

main

0x4011d6

_fini

0x40127c

5.7 Hello的动态链接分析

当程序调用一个由动态共享链接库定义的函数时,编译器在编译阶段无法预测这些函数在运行时的地址,因此编译系统采用了延迟绑定的方法,避免在运行时修改调用模块的代码段。链接器会在共享库中为这些函数添加重定位记录,等待动态链接器处理,将过程地址的绑定推迟到第一次实际调用该过程时。

在动态链接过程中,动态链接器使用过程链接表(PLT)和全局偏移量表(GOT)来实现函数的动态链接。GOT中存放着函数的目标地址,而PLT则利用GOT中的地址来跳转到目标函数。当程序首次调用共享库中的函数时,动态链接器会重定位GOT中的每个条目,确保它们包含正确的绝对地址。同时,PLT中的每个函数负责调用不同的目标函数。

执行init前:

       执行init后:

通过观察edb,我们可以发现dl_init.got.plt节发生的变化,这反映了动态链接器对GOTPLT的重定位和函数地址的解析过程。

5.8 本章小结

本章介绍了链接的概念与作用,通过使用edb工具以及阅读ELF文件,可以清晰地观察到程序的执行过程,包括程序地址的动态链接内容和相关函数调用。此外,本章深入分析了hello程序的重定位过程、执行流程和动态链接过程,对链接机制有了更深入的理解。

经过链接处理,我们得到了一个可执行文件。这不仅显示出链接技术的实用性,也体现了软件开发过程中各个环节之间的紧密联系和协同工作。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

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

进程的作用:

进程能够提供给应用程序的一些关键抽象:

  • 一个独立的逻辑控制流,它提供一个假象,好像程序能够独占使用处理器。
  • 一个私有的地址空间,它提供一个假象,好像程序能够独占使用内存系统。

进程是计算机科学中最深刻、最成功的概念之一。

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

Shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的Shell命令,那么Shell就会假设这是一个可执行文件的名字,它将加载并运行这个文件。在运行Hello时,Shell将加载并运行Hello程序,然后等待程序终止。程序终止后,Shell随后输出一个提示符,等待下一个输入的命令行。

6.3 Hello的fork进程创建过程

pid_t fork(void);

进程通过调用fork函数创建一个新的运行的子进程。子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中的内容,但它们有着不同的PID,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

fork只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。因为子进程的 PID 总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。

父进程和子进程还有以下的特点:

  • 并发执行。父进程和子进程是并发运行的独立进程。内核能够以任意方式交替执行它们的逻辑控制流中的指令,不同进程中指令的交替执行的顺序无法预判。
  • 相同但是独立的地址空间。两个进程的地址空间是相同的。每个进程有相同的用户栈、相同的本地变量值、相同的堆、相同的全局变量值,以及相同的代码。然而,因为父进程和子进程是独立的进程,它们都有自己的私有地址空间。父进程和子进程对变量所做的任何改变都是独立的,不会反映在另一个进程的内存中。

③ 共享文件。子进程继承父进程所有的打开文件。

6.4 Hello的execve过程

int execve(const *filename, const char *argv[], const char *envp[]);

execve过程发生在调用fork创建新的子进程之后。作用是在当前进程的上下文中加载并运行一个新的程序。filename是可执行目标文件,argv是参数列表,envp是环境变量列表。它调用一次,从不返回,只有出现错误时execve才会返回到调用程序。

6.5 Hello的进程执行

内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

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

上下文切换的步骤:

    • 保存当前进程的上下文;

② 恢复某个先前被抢占的进程被保存的上下文;

③将控制传递给这个新恢复的进程。

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

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

图34 进程上下文切换

(摘自《深入理解计算机系统》)

6.6 hello的异常与信号处理

6.6.1 正常执行

       每一秒输出一次,一共循环输出10次,按任意键退出。

图35 正常执行

6.6.2 执行过程中乱按

循环输出的结果不受影响。

图36 执行过程中乱按

6.6.3 键盘键入Ctrl-C

向shell传递SIGINT信号,使程序被终止,同时父进程使用waitpid函数等待子进程结束后,回收子进程。

图37 键盘键入Ctrl-C

6.6.4 键盘键入Ctrl-Z

       向shell传递SIGTSTP信号,程序处于挂起状态。

图38 键盘键入Ctrl-Z

       键入Ctrl-Z后,执行ps,结果如下图所示。

图39 执行ps

       再执行fg,挂起的程序继续执行,输出结束后,按任意键退出。

图40 执行fg

6.6.5 jobs

       键入Ctrl-Z将程序挂起,执行jobs,列出目前挂起的程序。

图41 执行jobs

6.6.6 进程树

       执行pstree,列出进程树。

图42 进程树

6.6.7 kill

       执行kill发送-9信号,杀死进程。

图43 执行kill

6.7本章小结

本章阐述了进程的概念及作用,简述了shell的作用与处理流程,同时对fork函数与execve函数进行了分析,给出了hello执行的过程,并对可能出现的异常进行了展示,对计算机系统的异常、进程、信号等知识进行了学习。

7章 hello的存储管理

7.1 hello的存储器地址空间

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

① 逻辑地址(Logical Address):逻辑地址是由程序生成的地址,在程序的编译或运行时产生。它是相对于某个基地址的偏移量,也被称为虚拟地址的一部分。hello程序中的代码指令和数据引用都是使用逻辑地址。

② 线性地址(Linear Address):线性地址是经过段式内存管理单元(MMU)转换后的地址,在段式内存管理中,逻辑地址通过段选择器和段偏移量转换为线性地址。在x86架构中,hello程序中的逻辑地址通过段寄存器转换为线性地址。

③ 虚拟地址(Virtual Address):虚拟地址是经过页式内存管理单元(MMU)转换后的地址,用于进程的内存空间管理。虚拟地址空间是每个进程独立的地址空间。hello程序在用户态执行时使用虚拟地址。

④ 物理地址(Physical Address):是实际的内存硬件地址,表示数据在物理内存中的具体位置。

在hello程序中,需要将各个指令的虚拟地址变为物理地址并完成各种操作,具体的过程为:先将hello虚拟地址或逻辑地址通过运算映射等方式得到线性地址,而后线性地址再通过页式管理变换的方式转变为物理地址,从而实现hello程序的相关执行。

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

逻辑地址由段标识符(也称为段选择子)和段内偏移量两部分组成。段标识符是一个16位长的字段,其中前13位作为索引号,用于在段描述符表(全局段描述符表GDT或局部段描述符表LDT)中直接查找对应的段描述符。而段标识符的后3位则包含了一些硬件细节。段描述符分为三类,全局的段描述符存储在GDT中,局部的段描述符存储在LDT中,而中断的描述符则放在中断描述符表(IDT)中。

逻辑地址到线性地址的转换是通过段式管理来实现的。首先从逻辑地址中提取出段选择子,然后根据这个选择子在GDTLDT中查找对应的段描述符,从中提取出段的基址信息,将这个基址与逻辑地址中的段内偏移量相加,得出的结果就是线性地址。线性地址是段基址和偏移量的组合,它直接表示了程序在内存中的位置。通过这个过程,逻辑地址被有效地转换为线性地址,CPU随后根据这个线性地址进行内存访问。

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

计算机利用页表,通过MMU(内存管理单元)完成从虚拟地址(也称为线性地址,用VA表示)到物理地址的转换。在这个过程中,VA首先被分为虚拟页号(VPN)和虚拟页偏移量(VPO)CPU取出VPN,并通过页表基址寄存器来定位页表条目。当页表条目的有效位为1时,CPU从页表条目中取出信息物理页号(PPN)。将PPNVPO结合,就能得到由物理页号(PPN)和物理页偏移量(PPO)组合而成的物理地址。此外,线性地址还可以被划分为目录索引、页表索引和页内偏移,操作系统会利用页目录表和页表进行地址映射,确保程序能够正确访问内存。这个转换过程由CPU和操作系统协同完成,实现了线性地址到物理地址的精确映射。

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

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存有一个单个PTE组成的块。TLB通常有高度的相连度,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。关键点在于所有的地址翻译步骤都是在芯片的MMU执行的,因此速度非常快。一般的处理流程为:

(1)CPU产生一个虚拟地址;

(2)MMU从TLB中取出相应的PTE;

(3)MMU将这个虚拟地址翻译成一个物理地址,并将其发送至高速缓存、主存;

(4)高速缓存、主存将所请求的数据字返回给CPU。

当进程运行时,它的页目录表地址被加载到CR3控制寄存器中。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址,CPU通过CR3得到页目录表的地址,进行地址变换。变换的过程是:先用页目录号作为索引,在页目录表中定位对应的表项,从中得到页表的页帧号。同样再以页表号为索引在页表中找到对应的页表项,从中得到被映射地址的页帧号。即:VPN1提供到一个L1PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2PTE的偏移量,以此类推依次向下。页帧号与页内位移相拼即得到物理地址。

图44 使用k级页表的地址翻译

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

物理内存访问首先访问L1缓存,如果未命中则访问L2缓存,再未命中则访问L3缓存,若L3缓存也未命中,则访问主内存读取数据。缓存命中时直接返回数据,未命中时通过逐级缓存填充机制将数据加载到各级缓存中,以加速后续访问,提高内存访问效率和系统性能。

7.6 hello进程fork时的内存映射

当Hello进程调用fork时,操作系统通过写时复制(copy-on-write)机制为子进程创建父进程内存空间的映射,子进程最初共享父进程的物理内存页,但页表条目被标记为只读。当父或子进程尝试写入共享内存页时,会触发页面异常,操作系统为该进程创建该页的副本,并更新页表,使两进程分别指向不同的物理内存页,从而实现独立的内存空间。

图45 一个私有的写时复制对象

(摘自《深入理解计算机系统》)

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行新程序a.out时:

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

② 创建新的区域结构,这些新的区域都是私有的、写时复制的,代码和初始化数据映射到.text和.data区,.bss和栈堆映射到匿名文件。

③ 映射共享区域。如果a.out程序与共享对象链接,那么这些对象都是动态链接到这个程序的,再映射到用户虚拟地址空间中的共享区域内。

④ 设置程序计数器(PC)。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

缺页故障是指当CPU尝试访问一个虚拟内存地址对应的物理页时,若该页不在内存中,则会触发的异常。操作系统首先检查引发缺页故障的页是否在页表中。如果该页在页表中不存在,则表示为无效引用,操作系统将终止程序;若页表中存在该页的信息,则操作系统会根据页表信息将缺失的页从磁盘调入内存,并更新页表。然后,CPU会重新执行引起缺页的指令,使程序能够继续执行。

当缺页故障发生时操作系统会调用缺页处理程序。在这个过程中,内存管理器会确定一个牺牲页来腾出空间。如果该页面在内存中已被修改过,它会被换出到磁盘中以保存更改。接着,新的目标页会被加载到内存中,替换掉牺牲页。完成这些步骤后,缺页处理程序会返回到原来的进程,并重启导致缺页的指令,确保程序能够继续执行。

7.9本章小结

本章阐述了计算机的虚拟内存管理,介绍了虚拟地址、物理地址、线性地址、逻辑地址的概念与区别。此外,还分析了虚拟地址被翻译成物理地址的过程,以及段式、页式的管理模式,介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理。

 

结论

Hello程序的一生,从诞生到终结,经历了从源代码到可执行文件的蜕变,也经历了从加载到执行再到结束的完整流程。

  • 编译阶段
    • 编写与存储:通过文本编辑器,将用高级语言编写的程序存储到hello.c文件中。
    • 预处理:预处理器对hello.c文件进行初步处理,生成hello.i文件。
    • 编译:编译器将hello.i文件编译成汇编代码,并保存在hello.s文件中。
    • 汇编:汇编器将hello.s文件中的汇编代码转换为机器语言指令,生成可重定位目标程序hello.o
    • 链接:链接器将hello.o与外部库或其他目标文件进行链接,生成最终的可执行文件hello
  • 运行阶段
    • 创建进程与加载:当shell收到运行./hello的指令后,通过fork创建子进程,并在子进程中调用execve来创建虚拟内存空间映射,利用高速缓存与缺页处理机制将hello加载到内存和CPU中。
    • 执行CPU从内存中取出指令并执行,控制hello的逻辑流。在此过程中,hello会调用如printfgetchar等函数与IO设备交互,实现屏幕的显示和键盘的读入。
    • 异常处理:对于如ctrl-cctrl-z等键盘输入的中断指令,系统会调用相应的信号处理程序进行处理。
    • 结束:当hello执行完所有任务后,父进程会回收子进程占用的资源,hello的一生就此结束。

感悟与思考

通过深入学习和梳理hello的一生,我深刻理解了现代计算机操作系统各部分之间的协作与调用机制,对系统各部分的设计思想、处理方式有了基本的认识。同时,也感受到了计算机系统的复杂性和严密性。hello程序虽然小,但其中蕴含的世界却异常丰富。它不仅是一个程序的自白,更是计算机专业学生初次接触计算机世界时,面对新事物的好奇与对计算机系统精妙和伟大的赞叹的集中体现。hello的一生,不仅是一个程序的生命周期,更是我们计算机学习之旅的起点。

附件

文件名称

文件说明

hello.c

源程序

hello.i

预处理生成的文本文件

hello.s

.i文件编译后得到的汇编语言文件

hello.o

.s文件汇编后得到的可重定位目标文件

hello

.o经过链接生成的可执行目标文件

elf.txt

hello.o的elf文件

asm.txt

hello.o的反汇编代码文件

helloelf.txt

hello的elf文件

参考文献

[1]  Bryant,R.E. 等. 深入理解计算机系统[M]. 北京: 机械工业出版社, 2016.

  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值