Hit csapp 大作业程序人生

 

本文介绍C语言程序设计课程中的经典案例——hello.c程序,从计算机系统的角度阐述其从源程序到可执行程序的转变,并探讨其在计算机系统中作为进程的运行过程。首先,源程序需要经过预处理、编译、汇编、链接等步骤,才能生成二进制可执行目标程序。其次,在运行程序的过程中,计算机系统的硬件组件,如处理器、I/O设备、主存等,与程序密切配合。同时在此过程中,也涉及到操作系统的进程调度和管理。本文将以hello.c为例,深入探讨其从程序到进程的演变过程,为读者提供对计算机系统运行程序的全面理解。

关键词:计算机系统;预处理;编译;汇编;链接;进程;存储                            

 

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

1.1.1 P2P

运行hello.c程序时,由编译器驱动程序启动,读取hello.c文件,然后进行预处理:得到预处理后的hello.i文件;之后由编译器对hello.i进行编译:得到一个汇编语言程序,即hello.s。之后将汇编程序交由汇编器进行汇编:得到一系列机器语言指令,并将这些机器语言指令打包成一种“可重定位目标程序”,并将其存入到hello.o(二进制)文件中。最后由链接器进行链接:结果就得到了可执行目标文件:hello。接下来计算机就可以运行这个hello文件了。之后在计算机的Bash(shell)中,OS会为hello创建子进程(fork),这样,在计算机系统中,hello就有了自己独一无二的进程(Process),在这个进程中hello便可以运行。

1.1.2 从零到零(020)

程序从无到有,通过编写、编译、链接等步骤最终形成可执行文件(从零到一),执行后进程终止,资源被回收(从一回到零),整个过程再次回到初始状态

1.2 环境与工具

X64 CPU;2GHz;2G RAM;256GHD Disk 以上

Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上

Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc

1.3 中间结果

原始代码hello.c。预处理后的代码hello.i

  • 编译后的汇编语言代码hello.s
  • 可重定位目标文件hello.o
  • 可执行文件hello

1.4 本章小结

本章主要介绍了hello.c程序P2P020的过程。列出了本次实验所需的环境和工具以及过程中所生成的中间结果。

(第10.5分)

2章 预处理

2.1 预处理的概念与作用

2.1.1 概念

预处理指的是程序在编译之前进行的处理,是计算机在处理一个程序时所进行的第一步处理,可以进行代码文本的替换工作,但是不做语法检查。 预处理是为编译做的准备工作,能够对源程序.c 文件中出现的以字符“#”开头的命令进行处理,包括宏定义# define、文件包含# include、条件编译# if def 等,最后将修改之后的文本进行保存,生成.i文件,预处理结束。

2.1.2 作用

对代码进行文本替换和宏扩展,从而生成最终的源代码文件,然后再由编译器进行编译。提高代码的复用性,使得代码更加清晰,便于维护和调试。

例如,#define PI 3.14,宏定义在预处理的过程中会进行宏替换。预处理器在编译前会将所有出现的宏名称替换为其定义的值或代码。使用#include指令可以将另一个文件的内容插入到当前文件中。可用于包含头文件(.h文件),从而共享函数声明、宏定义等代码片段。#include <stdio.h>会将标准输入输出库的头文件内容插入到当前文件中。#if#ifdef#ifndef#else#elif#endif指令,可以有选择地编译代码的某些部分

2.2在Ubuntu下预处理的命令

在终端内输入命令gcc -E hello.c,在屏幕上得到hello.c的预处理结果(如图)。为方便起见我们重定向gcc的输出,将结果保存到hello.i文件内。

2.3 Hello的预处理结果解析

查看hello.i文件,在最开头,是hello.c涉及到的所有头文件的信息

然后,先是这些头文件用typedef定义的诸多类型别名

然后的内容是被include的头文件们的主体内容,包括大量的函数声明和少部分struct定义等。它们都完全经过预处理器的宏展开

在文件的最后,才是真正的hello.c的内容

2.4 本章小结

hello.c的预处理后的结果我们可以看出,预处理器确实对源代码进行了大量的展开操作,预处理后的结果仍然是合法的C语言源文件,但它比原先要扩充了很多被include来的东西。因此我们从预处理步骤就可以初步发现,一个小小的hello world程序,其背后隐藏的东西要多得多……

(第20.5分)

3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

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

3.1.2 编译的作用

在编译阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。在编译阶段,编译器还能起到优化的作用,优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。

3.2 在Ubuntu下编译的命令

在终端里输入命令gcc -S hello.i -o hello.s,得到编译结果hello.s。如下图所示:

3.3 Hello的编译结果解析

3.3.1 数据

3.3.1.1常量

字符串常量,位于只读数据段(.rodata)

3.3.1.2变量(全局/局部/静态)

无全局与静态变量。

局部变量

局部变量通常使用栈指针(%rsp)和基址指针(%rbp)进行访问。在此段汇编代码中,可以看到通过基址指针(%rbp)来访问局部变量。

-20(%rbp):此偏移量用于存储从main函数参数argc传入的值。汇编代码中通过movl %edi, -20(%rbp)存储参数argc的值。

-32(%rbp):此偏移量用于存储从main函数参数argv传入的值。汇编代码中通过movq %rsi, -32(%rbp)存储参数argv的值。

-4(%rbp):存储局部变量i,用于在循环中计数。汇编代码中通过movl $0, -4(%rbp)初始化该变量。通过addl $1, -4(%rbp)递增该变量

3.3.1.3表达式

.c中的表达式argc!=5,.s文件中表示为

i<10表示为

3.3.1.4 类型

类型的解析通过汇编指令的选择和操作数的大小来体现。根据变量类型使用不同的mov指令。

3.3.1.5

无宏,若有宏,所有的宏替换也都在预处理阶段完成。

3.3.2赋值

将参数的值从寄存器(%edi %rsi)移动到了相对于基址指针 %rbp 的栈上的位置(偏移 -20 -32 处)。

赋值0i

3.3.3算术操作

i++addl $1,-4(%rbp)完成.

3.3.4关系操作

i<10: 并没有用10,而是用9在比较,若等于九后则跳转到下部分代码不再循环。

3.3.5数组/指针/结构操作

main接收的argv[]数组。

3.3.6控制转移

if:i不满足小于等于9,则跳出循环(leave)。

3.3.7函数操作

3.3.7.1 参数传递(地址/)

3.3.7.2 函数调用

通过call指令调用,如图,分别调用了头文件提供的printf, sleep, getchar函数。

3.3.7.3 函数返回

ret 指令用于将程序的控制权返回到调用该函数的位置,并且通常在函数的结尾处使用。指令.cfi_endproc 表示这是一个函数结束的标记,它用于通知调试器和其他工具函数的结束位置

3.4 本章小结

本章hello.i -> hello.s,直观地看到了编译的结果,并将起与C源程序的代码结合起来,理解汇编语言发挥的作用,以过往的实验经历,也可以很熟练地将汇编代码与对应的C语言代码对照

(第32分)

4章 汇编

4.1 汇编的概念与作用

4.1.1 概念

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

4.1.2 作用

1.底层控制:汇编语言可以直接操作计算机的硬件,包括CPU、内存、输入输出设备等。因此,它在对计算机进行底层控制和操作时非常有用。

2.性能优化:由于汇编语言直接操作硬件,可以更加精细地控制程序的执行过程,从而实现对程序的性能优化。

4.2 在Ubuntu下汇编的命令

在终端中对hello.s使用命令as -o,如图所示:

4.3 可重定位目标elf格式

ELFExecutable and Linkable Format)文件是一种常见的可执行文件和可链接文件格式,用于在Unix和类Unix系统上存储程序、库和其他相关数据。包括文件头、程序头表、节表、节、重定位表、符号表。

使用readelf解析汇编器生成的可重定位目标文件hello.o,结果如下:

4.3.1 文件头

首先查看文件头(ELF Header)。其包含了描述ELF文件整体结构和属性的信息,包括ELF标识、目标体系结构、节表偏移、程序头表偏移等。

4.3.2 程序头表

对于目标文件,程序头表(Program Header Table)可能为空。查询后确实如此

4.3.3 节表

节表描述了ELF文件中各个节的信息,包括节的名称、类型、偏移、大小等。ELF文件的数据和代码通常存储在各个节中,比如.text节存储代码段、.data节存储数据段等。其中.text.data是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。

4.3.4

readelf -a hello.o探查ELF文件中能探查的其他节,.rela.text 包含 8 个条目。以下是这些条目的详细信息

4.3.5 重定位表

重定位节.rela.eh_frame记录了需要进行地址重定位的异常处理框架(exception handling frame)信息。以下是查看.rela.eh_frame的输出。

4.3.6 符号表

符号表 .symtab 是目标文件中非常重要的一部分,它列出了所有定义的符号,包括函数、变量和节。符号表在链接和调试过程中起着至关重要的作用。以下是符号表 .symtab 的具体内容和解释。

4.4 Hello.o的结果解析

在终端中输入命令objdump -d -r hello.o,得到hello.o中可执行代码的反汇编结果。

对照分析:

每行代码末尾指令基本相同,但在每条指令前面都会有一串十六进制的编码。hello.s是由汇编语言组成的,相对于计算机能识别的机器级指令,汇编代码仍是抽象语言;而反汇编得到的代码不仅仅有汇编代码,还有机器语言代码。机器语言代码是计算机可识别执行的,是一种纯粹的二进制编码。

分支转移时,.s文件中会跳转到诸如.L3的代码段,像这样。

而反汇编文件中,则是直接跳转到当前过程的起始地址加上偏移量得到的直接目标代码地址。

函数调用时,汇编文件中,call指令直接调用函数,call后紧跟函数的名字

反汇编文件中,86: 重定位条目所影响的地址偏移量,这是call指令中地址字段的起始位置。R_X86_64_PLT32: 重定位类型,这里是R_X86_64_PLT32,表示这是一个 32 位的 PLT(Procedure Linkage Table)重定位。PLT 用于延迟绑定函数调用,在运行时解析函数地址。atoi-0x4: 表示重定位的目标符号是 atoi,加数是 -4。call指令执行时,会跳转到由重定位条目所指示的目标地址。重定位类型R_X86_64_PLT32指示链接器,地址字段需要被填充为指向PLT表项的偏移,这个表项在运行时会解析为atoi函数的实际地址。atoi-0x4中的 -0x4 是因为call指令的目标地址是相对于下一条指令的,所以需要减去4个字节来调整指令位置。

4.5 本章小结

汇编这一步骤使得hello程序真正开始从文本状态转化为二进制状态,但我们一定要明白这并不是简简单单地翻译为真正的机器码,而是生成可重定位的机器码,重定位这一奇妙的机制是为了供链接使用的。

(第41分)

5章 链接

5.1 链接的概念与作用

5.1.1概念

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以用于编译时,即将源代码翻译为机器码时,加载时,即程序被加载器加载到内存并执行时,还可执行于运行时,也就是用应用程序执行。现代系统中,链接是由叫做链接器(linker)程序自动执行的。

5.1.2 作用

使得分离编译成为可能,不用将大型程序组织为一个巨大的源文件,而是可以把他分解为更小、更好管理的模块,可以独立地修改和编译这些模块。链接可以帮助我们构造大型程序、帮助我们避免一些危险的编程错误、帮助我们理解语言的作用域规则如何实现、理解其他重要的系统概念、能够利用共享库。

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.3.1 ELF头信息

5.3.2 节头

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

5.3.3 程序头

5.4 hello的虚拟地址空间

打开edb,通过 data dump 查看加载到虚拟地址的程序代码。查看 ELF 格式文件中的程序头,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。在下面可以看出,程序包含PHDR,INTERP,LOAD ,DYNAMIC,NOTE ,GNU_STACK几个部分,PHDR 保存程序头表。INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据、程序的目标代码等。DYNAMIC 保存了由动态链接器使用的信息。NOTE 保存辅助信息。GNU_STACK:权限标志,用于标志栈是否是可执行。与5.3对照,我们可以根据5.3中每一节对应的起始地址在edb中找到响应信息。如下图所示

5.5 链接的重定位过程分析

在终端里输入命令objdump -d -r hello,得到可执行文件hello的反汇编结果。这个与可重定位目标文件hello.o的反汇编的结果相比,主要有两个地方不同,一是扩充了很多函数代码,包括程序加载后执行main前的一些准备工作,以及hello需要用到的一些库函数的定义等;二是在main中,原本在hello.o中等待重定位而暂时置0的地址操作数被设置为了虚拟地址空间中真正的地址。

所以,链接器首先会将所有模块的节都组织起来,为可执行文件的虚拟地址空间定型,再根据这个虚拟地址空间将那些存在hello.o里的.rel.text.rel.data节的重定位条目指向的位置的操作数都设置为正确的地址。

5.6 hello的执行流程

程序地址

程序名

0x0000000000401100

hello!start

0x0000000000401125

hello!main

0x0000000000401000

hello!_init

0x0000000000401140

hello!_fini

5.7 Hello的动态链接分析

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。[1]

.got节用于动态链接。它是一个包含了所有全局变量和函数的地址表。当程序运行时,动态链接器会更新这个表,以便程序可以正确地访问这些全局变量和函数。它的主要目的是支持位置无关代码(Position-Independent Code, PIC),使得代码可以在不同内存地址加载而不需要重新编译。

.got.plt节与.got类似,但专门用于延迟绑定(Lazy Binding)的函数调用。延迟绑定是一种优化技术,只有在函数第一次调用时才进行符号解析和重定位。在函数第一次调用后,.got.plt中的条目会更新为实际的函数地址,后续调用会直接跳转到该函数,提高了运行时性能。

5.8 本章小结

通过以上操作,我了解了hello.o、静态库、动态链接库这三者是如何通过链接机制组合在一起的,同时也初步探索了一个C语言程序从被加载到程序退出的全过程,我们会发现静态库和动态链接库的部分在我们看不见的地方起到了很大的作用,所以hello这一程序背后确实比表面上要复杂的多。这些都要归功于链接机制,我们才能十分方便地借助这些库来编写我们的程序,使其正常地在操作系统提供的平台上运行。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

6.1.1 概念

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

6.1.2 作用

进程提供给应用程序的关键抽象:一个独立的逻辑控制流;一个私有的地址空间。通过逻辑控制流和私有地址空间的抽象,进程提供给用户一种假象:就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接着一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

6.2.1 作用

shell是一个交互型应用级程序,代表用户运行其他程序。如Windows下的命令行解释器,cmd、powershell,图形界面的资源管理器。Linux下的Terminal/tcsh、bash等等,也包括图形化的GNOME桌面环境。Shell是信号处理的代表,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。

6.2.2 处理流程

1. Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符 号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:

SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |

2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。

3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令 的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程 回到第一步重新分割程序块tokens。

4.Shell对~符号进行替换。

5.Shell对所有前面带有$符号的变量进行替换。

6. Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用$(command) 标记法。

7.Shell计算采用$(expression)标记的算术表达式。

8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割 符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。

9.Shell执行通配符* ? [ ]的替换。

10.shell把所有从处理的结果中用到的注释删除,并且按照下面的顺序实行命令的检查:

A.内建的命令

B. shell函数(由用户自己定义的)

C.可执行的脚本文件(需要寻找文件和PATH路径)

11.在执行前的最后一步是初始化所有的输入输出重定向。

12.执行命令。

6.3 Hello的fork进程创建过程

在shell接受到./hello这个命令行后,它会对其进行解析,发现是加载并运行一个可执行文件的命令,于是它会先创建一个对应./hello的作业,再用fork()创建一个子进程,这个子进程与父进程几乎与父进程完全相同,它们有着相同的代码段、数据段、堆、共享库以及栈段,但它们的pid与fork的返回值是不同的,因此可以进行区分。然后,父进程(即shell主进程)会将新创建的子进程用setpgid()放在一个新的进程组中,这样这个进程组就对应./hello这个作业,shell可以通过向进程组中的所有进程发信号的方式管理作业。

6.4 Hello的execve过程

在这个子进程创建出来后,子进程会去调用execve()来加载可执行文件到当前进程,这样,子进程原来的用户区域将会被删除,然后通过虚拟内存机制将可执行文件hello中的各个段映射到对应的代码段、数据段等地址空间,这样就加载了hello的新的用户区域。然后,execve会加载hello用到的共享库(比如上面提到过的ld-2.31.so),也是通过虚拟内存映射的方式。最后,子进程的程序将直接跳转到hello的入口点,进行hello的执行

6.5 Hello的进程执行

进程正常运行是依赖于其上下文的,上下文是由程序正确运行的状态组成的,这些状态包括存放在内存里的程序的代码、数据、栈、寄存器、所占用的资源等,总之,在程序正常运行的时候,这些上下文状态绝不能被异常破坏。

然而,进程是需要不断进行切换的。当前运行在CPU的进程每隔一段时间就需要切换至其它进程。假设hello进程现在正在运行,突然发生了由主板上的时钟芯片引发的时钟中断(属于异步异常),然后处理器会从用户态立刻转入到内核态(拥有最高的管理特权级),控制流转入操作系统内核程序,内核会将hello进程目前的上下文暂时保存起来,然后通过进程调度程序找到要切换的进程B,加载B的被保存的上下文,将控制流交给进程B,处理器重新转入到用户态。

并且,操作系统会给每个进程分配时间片,它决定了当前进程能够执行它的控制流的连续一段时间。

在hello程序被执行的时候,初始时正常运行,然后hello调用sleep函数,这时sleep通过syscall引发异常(陷阱),转入内核态,内核保存hello的上下文,然后将hello进程置于休眠态,切换到其它进程。等到休眠时间到了的时候,此时时钟中断使得控制流从其它进程跳到内核,内核发现hello进程的休眠时间到了,就把hello解除休眠状态。之后在应当进行进程切换的时候,恢复hello的上下文,控制流转入hello进程,处理器切换到用户态。

6.6 hello的异常与信号处理

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

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

按下Crtl+Z,进程收到SIGSTP信号,hello进程挂起并向父进程发送SIGCHLD

运行ps命令查看进程运行状态

运行jobs命令,可以看到停止的作业

pstree,显示所有运行中的进程的树状图

Kill,杀死程序

Ctrl+C发送SIGINT信号,Hello进程被终止

6.7本章小结

本章介绍了进程的概念和作用,观察了hello进程的创建,执行,终止以及各个命令的执行,如进程树,ps等。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是程序直接使用的地址,它表示为“段:偏移地址”的形式,由一个段选择子(一般存在段寄存器中)再加上段内的偏移地址构成。

线性地址(或者叫虚拟地址)是虚拟内存空间内的地址,它对应着虚拟内存空间内的代码或数据,表示为一个64位整数。

物理地址是真正的内存地址,CPU可以直接将物理地址传送到与内存相连的地址信号线上,对实际存在内存中的数据进行访问。物理地址决定了数据在内存中真正存储在何处。

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

对于一个以段:偏移地址形式给出的逻辑地址,CPU将会通过其中的16位段选择子定位到GDT/LDT中的段描述符,通过这个段描述符得到段的基址,与段内偏移地址相加得到的64位整数就是线性地址。这就是CPU的段式管理机制,其中,段的划分,也就是GDTLDT都是由操作系统内核控制的。

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

虚拟地址空间会被分为若干页,即分页机制。CPU对于一个线性地址会取它的高若干位,通过它们去存储在内存中的页表里查询对应的页表条目,得到这个线性地址对应的物理页起始地址,然后与线性地址的低位(页中的偏移)相加就是物理地址。

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

在现代操作系统中,地址转换过程中除了页表机制外,还使用了转换后备缓冲(Translation Lookaside Buffer, TLB)来加速虚拟地址(VA)到物理地址(PA)的转换。下面我们详细说明TLB和四级页表机制下虚拟地址到物理地址的转换过程。

7.4.1 TLB

TLB是一种高速缓存,用于存储最近使用的虚拟地址到物理地址的映射。通过TLB,可以避免每次地址转换都进行多级页表查找,从而加速地址转换过程。

7.4.2 四级页表机制

四级页表机制将虚拟地址转换为物理地址时,通过四级页表结构进行映射。假设我们使用64位地址空间,地址分解如下:

Page Map Level 4 (PML4):PML4是四级页表的顶层,每个进程有一个PML4表。

Page Directory Pointer Table (PDPT):PDPT是第二级页表,它的每个条目指向一个Page Directory (PD)。

Page Directory (PD):PD是第三级页表,它的每个条目指向一个Page Table (PT)。

Page Table (PT):PT是第四级页表,它的每个条目指向一个物理页框。

每一级页表大小为512项,每项指向下一级页表或物理页。

64位虚拟地址分为以下几部分:PML4 索引高9位,PDPT 索引下一个9位,PD 索引再下一个9位,PT 索引最后一个9位,页内偏移12位

7.4.3 TLB和四级页表结合的地址转换过程

(1)从虚拟地址(VA)提取各级索引和页内偏移

假设虚拟地址为 VA,PML4 索引位于高9位,PDPT 索引位于下一个9位,PD 索引位于再下一个9位,PT 索引为最后一个9位,页内偏移& 0xFFF为低12位。

(2)CPU首先在TLB中查找虚拟地址的映射。如果命中(TLB hit),则直接使用缓存的物理地址。如果未命中(TLB miss),则需要进行页表查找。

(3)四级页表查找(在TLB miss的情况下):

使用PML4索引在PML4表中查找,找到对应的PDPT表地址。使用PDPT索引在PDPT表中查找,找到对应的PD表地址。使用PD索引在PD表中查找,找到对应的PT表地址。使用PT索引在PT表中查找,找到物理页框地址。

(4)计算物理地址:

物理地址 = 物理页框地址 + 页内偏移

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

由于CPU对内存的访问较慢,CPU为内存访问提供了更快的三级Cache缓存,之前访问过的内存块将被暂存在缓存中。当CPU对一个物理地址进行访问的时候,首先去看L1 Cache里是否有对应内存块,若有则直接访问L1 Cache,否则去看L2 Cache……若三级Cache里都没有对应内存块,那么CPU将会直接访问物理内存,并将物理内存中的块加载到L3 L2 L1 Cache中,并使用最近最少访问策略替换掉Cache中的某个内存块。

7.6 hello进程fork时的内存映射

当shell使用fork创建子进程时,内核为新的子进程创建各种数据结构,并分配给子进程一个唯一的PID,为了给它创建虚拟内存空间,内核创建了当前进程的mm_struct、区域结构和页表的原样副本,将两个进程的页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。这样,在新进程里,最开始的时候它的虚拟内存和原进程的虚拟内存映射相同,但当这两个进程中的任意一个进行写操作时,写时复制机制就会创建新页面,这样两个进程的地址空间就在逻辑上私有了。

7.7 hello进程execve时的内存映射

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

pathname新程序的路径。

argv[]:传递给新程序的命令行参数。

envp[]:传递给新程序的环境变量。

1、 输入 ./hello flw 2022111600

2、 execve加载hello程序后,设置栈,将控制传递给hello程序的主函数。

3、 删除已存在的用户区域

4、 映射新的私有区域。代码和初始化数据映射到.text和.data区(执行可执行文件提供),.bss映射到匿名文件,共享对象由动态链接映射到本进程共享区域,设置PC,指向代码区域的入口点。栈中从栈底到栈顶是参数和环境字符串,再往上是指针数组,每个指针指向刚才的环境变量和参数字符串。栈顶是系统启动函数libc_start_main的栈帧和预留的未来函数的栈帧。

当hello进程调用execve()时,整个进程的内存映射发生了彻底的变化。旧的地址空间被清除,新的程序被加载到地址空间中。通过这种方式,execve()可以在当前进程的上下文中运行一个新的程序,而不需要创建新的进程。这样不仅节省了资源,还允许新程序继承当前进程的许多属性,如进程ID、环境变量等。该函数成功运行正确运行时不返回。逻辑控制流交给要运行的程序

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

7.8.1 缺页故障(Page Fault)

虚拟内存在DRAM缓存不命中即为缺页故障。

7.8.2 缺页中断处理

缺页中断处理:触发缺页异常时启动缺页处理程序

1、缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。

2、缺页处理程序页面调入新的页面,并更新内存中的PTE

3、缺页处理程序返回到原来的进程,再次执行导致缺页的命令

7.9动态存储分配管理

7.9.1 动态内存管理的基本方法

虽然可以使用低级的mmap和munmap函数来创建和删除虚拟内存区域,但是C程序员还是会觉得当运行时需要额外虚拟内存时,用动态内存分配器更方便,也有更好的可移植性。

(1)显式分配器

要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符与c中的malloc和free相当。

(2)隐式分配器

要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集,例如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

7.9.2 动态内存管理的策略

(1)带边界标签的隐式空闲链表

带边界标签的隐式空闲链表使用边界标签(boundary tags)来管理内存块,内存块之间没有显式的指针链接。每个内存块包含头部和尾部的边界标签,这些标签存储块的大小和状态(分配或空闲)。

(2)显示空间链表

显式空闲链表使用链表来维护所有空闲块,链表中的每个节点都包含指向下一个和上一个空闲块的指针。这种方式提供了更高效的空闲块管理。

7.10本章小结

揭示了hello的看似简单的内存访问背后的复杂机制,尤其是极度重要的基于页式管理的虚拟内存机制。对于某个地址处的数据访问,要涉及到基于段描述符的逻辑地址到线性地址的转换、基于分页机制的线性地址到物理地址的转换、TLB与Cache、缺页故障等机制,而虚拟内存空间能够使得程序在表面上独占整个内存。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux中,所有的IO设备(网络、磁盘、终端等)都被模型化为文件,所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O接口

Unix I/O接口,使得所有的输入和输出都能以一种统一且一致的方式来执行:

(1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件;Linux shell创建的每个进程开始时都有三个打开的文件,标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2);

(2)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0;

(3)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n;类似地,写操作就是从内存复制n>0个字节到文件,从当前文件位置k开始,然后更新k;

(4)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。

8.2.2 Unix I/O函数

(1)进程通过调用open函数来打开一个已存在的文件或者创建一个新文件的:int open(char *filename, int flags, mode_t mode)

open函数将filename转换为一个文件描述符,并且返回描述符数字;flags参数也可以是一个或者更多位掩饰的或,为写提供给一些额外的指示;mode参数指定了新文件的访问权限位。

(2)close函数

进程通过调用close函数关闭一个打开的文件。

(3)read函数

应用程序是通过分别调用read来执行输入,ssize_t read(int fd, void *buf, size_t n);

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0比怕是EOF。否则返回值表示的是实际传送的字节数量。

(4)write函数

应用程序是通过调用write函数来执行输出,ssize_t write(int fd, const void *buf, size_t n);

write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

(5)lseek函数

通过调用lseek函数,应用程序能都显示地修改当前文件的位置。

8.3 printf的实现分析

printf函数的代码如下:

首先,printf开辟一块输出缓冲区,然后用vsprintf在输出缓冲区中生成要输出的字符串。之后通过write将这个字符串输出到屏幕上。而write会通过syscall陷阱跳到内核,内核的显示驱动程序会通过这些字符串及其字体生成要显示的像素数据,将它们传到屏幕上对应区域的显示vram中。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar的实现大致如下:

首先,getchar会开辟一块静态的输入缓冲区,若输入缓冲区为空,则调用read向输入缓冲区中读入一行字符串。而read会通过syscall陷阱跳到内核,内核会使得调用方不断等待。当按下键盘后,键盘中断处理程序执行,向输入缓冲区中放入由键盘端口读入的扫描码转换成的字符,直到按下回车后调用方不再等待。那么getchar所做的事情其实就是不断地从输入缓冲区中取下一个字符,如果没有则等待输入。

8.5本章小结

通过以上分析,我们从底层的输入输出机理揭示了hello是如何在屏幕上打印出信息的,又是如何接受键盘输入的,它们背后的机制是软件通过底层IO端口或中断与外部硬件的交互。

(第81分)

结论

(1)hello.c在预处理之后,将头文件的内容插入到程序文本中,得到hello.i;

(2)编译器对hello.i进行编译,从而得到汇编文件hello.s;

(3)经过汇编器汇编,得到与汇编语言一一对应的机器语言指令,在汇编之后,得到了可重定位目标文件hello.o,其是一个二进制文件;

(4)链接器对hello.o中调用函数的指令进行重定位,将调用的系统函数如printf.o等链接到hello.o,得到可执行目标文件hello;

(5)在计算机运行hello。首先在shell-Bash中输入符合要求的语句,运行hello的命令行./hello lmy 2022113064 3,OS就fork()为hello创建一个子进程,hello就在这个进程当中运行;

(6)运行时,首先,在hello中的地址为虚拟地址,要经历虚拟地址映射为线性地址,线性地址映射到物理地址,才能对该地址进行操作;

(7)hello程序正常运行,输出结果,过程中通过文件管理I/O设备;

(8)最后由shell父进程回收终止的hello进程。

感想:hello是学习c语言时接触的第一个程序,在没有了解计算机系统时,觉得他非常简单。但在做这次大作业时,从编译到汇编到链接到之后种种,一个小小的hello居然要经过这么多道关卡,最后才能变成最后呈现给我们在屏幕上的短短一行,再结合平时课上所学,让我不禁感叹计算机系统的精妙。在未来的学习与工作中,我也将不忘在计算机系统学到的知识,继续探索,更深入的了解计算机。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

 源文件

  可执行目标文件

  预处理后的修改的c程序

  可重定位目标文件

  汇编程序

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

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

(参考文献0分,缺失 -1分)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值