HIT 2024春csapp程序人生大作业

计算机科学与技术学院

2024年5月

摘  要

本文详细阐述了hello.c程序在Linux系统从编写到运行的完整生命周期。首先使用专业工具深入分析了程序在Linux环境下经历的预处理、编译、汇编和链接等各个阶段的具体过程与原理。接着探讨了应用程序在运行时和结束时涉及的进程管理、内存分配与回收、以及输入输出(IO)操作等关键方面。此外,本文还介绍了shell在Linux系统中的核心管理功能,包括内存管理、IO管理以及进程管理,并深入探讨了虚拟内存和异常信号等核心概念,全面理解Linux系统下程序运行的内部机制。

关键词:计算机系统;计算机体系结构;hello.c;Linux                      

目  录

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

P2P:P2P指的是程序由一个项目变成一个进程的过程.

1.Program:Hello程序的诞生是程序员通过键盘输入得到hello.c

2.Process:C语言源程序hello.c在预处理器(cpp)处理下,得到hello.i,通过编译器(ccl),得到汇编程序hello.s,再通过汇编器(as),得到可重定位的目标程序hello.o,最后通过链接器(ld)得到可执行的目标程序hello。在shell中键入运行命令后,shell调用fork函数为其创建子进程。

020:020为程序“从无到有再到无”的过程。程序经过系统OS,shell为hello进程execve,映射虚拟内存,进入程序入口后程序开始载入物理内存。进入 main 函数执行目标代码,CPU为执行文件hello分配时间周期,执行逻辑控制流,每条指令在流水线上取值、译码、执行、访存、写回、更新PC。当程序运行结束后, shell 父进程负责回收 hello 进程,内核删除相关数据结构。Hello程序从无到有再到无的这一过程就是020。

1.2 环境与工具

硬件环境:11th Gen Intel(R) Core(TM) i7-1195G7 @ 2.90GHz   2.92 GHz,16.0 GB RAM

软件环境:Windows 11 23H2 Ubuntu 20.04

开发工具:Vim、gdb、visual studio。

1.3 中间结果

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

1.4 本章小结

本部分对hello从诞生到执行到消亡的P2P和020过程进行了简介,并介绍了整个过程中所使用的环境工具及生成的中间结果。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:预处理阶段是编译过程的前置步骤,它通过预处理器(cpp)根据以#字符开头的指令,对原始的C程序进行修改。例如,对于#include<stdio.h>,#include 指令要求预处理器读取系统头文件 stdio.h的内容,并将其直接嵌入到程序文本中。这样就生成了另一个C程序,通常以.i为文件扩展名。

预处理的作用:预处理的作用主要可分为以下三部分:

(1) 宏展开:预处理程序中的“#define”标识符文本,用实际值(可以是字符 串、代码等)替换用“#define”定义的字符串;

(2) 文件包含复制:预处理程序中用“#include”格式包含的文件,将文件的内 容插入到该命令所在的位置并删除原命令,从而把包含的文件和当前源文 件连接成一个新的源文件,这与复制粘贴类似;

(3) 条件编译处理:根据“#if”和“#endif”、“#ifdef”和“#ifndef”后面的 条件确定需要编译的源代码。

2.2在Ubuntu下预处理的命令

预处理的命令为:gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

预处理后,文件的格式仍为文本文件。

文件的行数增加到了三千多行,其中最后几行才是原程序对应的内容,可以看出原来的c文件中的注释都被删除,余下的原程序部分没有发生任何变化。

2.4 本章小结

本章介绍了预处理的概念和作用,然后介绍了Linux系统下的两种预处理命令,最后通过查看hello.i中文件中的实际内容了解了预处理的实际结果,通过分析结果,加深了对于预处理的理解。

第3章 编译

3.1 编译的概念与作用

概念:编译是把通常为高级语言的源代码(这里指经过预处理而生成的 hello.i)到能直接被计算机或虚拟机执行的目标代码(这里指汇编文件 hello.s)的翻译过程。    

作用:

1. 词法分析,词法分析也称作扫描,是编译器的第一个步骤,词法分析器读入组成源程序的字符流,并且将它们组织成为有意义的词素的序列,对于每一个词素,词法分析器产生如下形式的词法单元作为输出。

2. 语法分析,语法分析器使用词法分析器生成的各词法单元的第一个分类来 创建树形的中间表示,在词法分析的基础上将单词序列组合成各类语法短语。该中间表示给出了词法分析产生的词法单元的语法结构,常用的表示方法为语法树。

3. 语义分析,语义分析器使用语法树和符号表中的信息来检查源程序是否和 语言定义的语义一致,它同时收集类型信息,并存放在语法树或符号表中, 为代码生成阶段做准备。

4. 代码生成和优化,在源程序的语法分析和语义分析完成后,会生成一个明 确的低级的或类及其语言的中间表示。代码优化试图改进中间代码,使生成的代码执行所需要时间和空间更少。最后代码生成以中间表示形式为输入,并把它映射为目标语言。

预处理过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位--预处理记号用来支持语言特性。预处理的功能可以分为宏定义,文件包含,条件编译三方面,由预处理器解析宏定义命令、文件包含命令、条件编译命令。为进一步编译提供准备文件。

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

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

       3.3.1 数据

包括常量、变量(全局/局部/静态)、表达式、类型、宏 。

本程序中只有常量和局部变量。

常量:1)字符串常量。c程序里有两处:"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n"

可以看出,LC1里明显存放了"Hello %s %s\n"这个字符串,;而另一个字符串在汇编代码里的存在形式不太明显,但是通过:Hello可以看出字符串"用法: Hello 学号 姓名 秒数!\n"存在了LC0处。

2)数字常量

在汇编语言中,$后面加上数字表示立即数。如c程序中argc!=5中的5就表示为这条汇编语句中的 $5

3.3.2赋值

  赋值操作通过数据传送指令来进行,主要是MOV类指令,如movb,movw,movl,movq

3.3.3算术操作

包括+ - * / %  ++  --     取正/负+-   复合“+=”等

本程序里只涉及了i++,其对应的汇编代码为

3.3.4关系操作和控制转移:

关系操作包括==    !=      >    <      >=     <=等。

控制转移包括if/else switch for while  do/while  ?: continue  break等。

本程序中涉及了if(argc!=5)和for(i=0;i<10;i++)

对应的汇编语句分别为:

这两条语句将argv和5进行比较,如果相等,则if(argc!=5)中的条件不成立,跳转到L3(else部分),否则继续顺序执行。

这两条语句将i和9进行比较,如果i<=9,则i跳转到L4(循环体部分),否则结束循环。对应了for(i=0;i<10;i++)中的i<10。Hello.c中写的是i<10但汇编中是i<=9说明编译器可能会自动寻找条件的等价实现。

cmp  A,B 根据B-A的计算结果设置条件码寄存器ZF/SF/CF/OF,但不更新目的寄存器B。可以通过对条件码的组合进行判断来实现关系运算。例如,!=可以通过判断~ZF实现,<可以通过作减法(不改变值)后判断SF^OF实现。条件跳转指令就是利用条件码的组合判断大小关系是否满足来决定是否跳转,如je就是如果ZF=1(说明之前的减法或cmp的结果0即两数相等)则跳转。

3.3.5数组/指针/结构操作:

包括A[i]    &v   *p    s.id    p->id等。

本程序中的例子是

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

sleep(atoi(argv[3]));

都是数组操作,以数组首地址为基址,使用变址寻址或比例变址寻址方式就可以实现对数组元素的表示。

由于main函数的实际结构为: int main(int argc,char* argv[]),

且寄存器传递参数的顺序是 %rdi,%rsi,%rdx,%rcx.......

因此argc在%rdi中,argv在%rsi中。

argv是argv[]数组的首地址,由movq %rsi, -32(%rbp)可知argv存在了-32(%rbp)里。

3.3.6函数操作

包括参数传递(地址/值)、函数调用()、局部变量、函数返回等。

参数传递:第1~6个参数存放在寄存器%rdi,%rsi,%rdx,%rcx,%r8,%r9中,寄存器不够用会把多的参数存放在栈中。

函数调用:使用call指令。call执行的过程首先把下一条指令地址压入栈中作为返回地址,然后把PC的值更新为call后面的符号表示的地址。通过ret返回时把PC设置为栈中之前存放的指令地址值即可。

局部变量:寄存器组是唯一被所有过程共享的资源,为了保证被调用者不会覆盖调用者的寄存器值,x86-64把寄存器分为两种:

被调用者保存寄存器:rbp,rbx和r12~r15。当P调用Q时,Q必须“保存”这些寄存器的值——要么不去使用,要么压入栈中然后返回前恢复。

调用者保存寄存器:剩下的所有的寄存器除了rsp都属于调用者保存寄存器,如果想要确保调用过程中这些值不被破坏那么调用者必须主动保存它们。

函数返回:使用ret指令。ret把PC设置为栈中之前存放的返回地址。返回值存放在%rax中,接下来以sleep(atoi(argv[4]));为例:

对应的源程序为sleep(atoi(argv[4]));由于已知argv存在了-32(%rbp)里,因此前四条语句取出-32(%rbp)+24处的值,即为argv[4],放入%rdi,这样就准备好了atoi的参数,call atoi调用atoi函数,其返回值存在%rax里,紧接着将%eax中的值传给%edi并调用sleep,这样就把atoi的返回值作为sleep的第一个参数传了进去。

3.4 本章小结

本章的目的是探讨编译的原理和实践,以及C语言与汇编语言之间的对应关系。首先,介绍了编译的定义和功能,即将文本文件转换为汇编语言程序,为生成机器码做好准备。其次,介绍了在Ubuntu环境下使用gcc命令对hello.c源文件进行编译,并生成hello.s汇编文件。然后,对hello.s文件中的各种数据类型和操作进行了详细的分析和解释,展示了编译器如何处理不同的数据和指令。通过本章的学习,加深了对C语言和汇编语言的理解,为后续的链接、加载和运行过程打下了基础。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编是一种面向机器的程序设计语言,它用易于理解和记忆的符号或名称来表示机器指令的操作码和操作数。汇编语言与机器语言之间有一一对应的关系,可以通过汇编器将汇编语言翻译成机器语言,也可以通过反汇编器将机器语言还原成汇编语言。汇编语言的优点是程序执行效率高,缺点是难于编写和调试,且与具体的处理器密切相关。

汇编的作用:

(1)适应不同的高级语言和编译器,提供通用的输出语言。

(2)便于分析和调试底层的问题,查看反汇编代码和机器状态。

(3)提高程序的执行效率和性能,利用处理器的特殊指令和优化技巧。

(4)理解计算机的工作原理和结构,掌握数据的存储格式和寻址方式。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式

  在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:

  结构分析:

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

2.节头部表:是描述目标文件的节,它描述了不同节的名字、类型、位置和大小。

3.可重定位节和符号表:.rel.text表示可重定位代码,它是一个.text节中位置的列表,当后续链接器把这个目标文件和其它文件组合时,需要修改这些位置。而.symtab表示符号表,它存放在程序中定义和引用的函数和全局变量的信息。hello.elf中的可重定位节和符号表如图:

4.4 Hello.o的结果解析

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

对照分析:

  1. 机器指令

对比hello.o的反汇编文件dump.txt和hello.s,可以发现两者在汇编指令上无太大差别,但反汇编文件有机器指令,由操作码和地址码构成,每条机器指令最前面有指令的相对地址。

  1. 分支转移

在hello.s文件中,分支转移的目的地是使用段名标出的,而经过汇编后分支跳转有了具体确定的地址偏移。

  1. 函数调用

和分支转移一样,在hello.s文件中,函数调用的具体目的地是用函数名给出的,而在汇编之后,函数调用有了具体的地址,是该条命令的下一条命令的地址。但这显然不正确,因为调用的函数还在其它库中,其具体调用的地址无法确定,只有在经过链接后函数调用的准确位置才能确定。

4.5 本章小结

本章首先从理论上阐述了汇编的概念和作用。之后以hello.o程序为例,详细分析了汇编前后程序的变化。可看出汇编后产生了可重定位目标文件,这其中有可供计算机理解的机器代码,同时,之前很多不确定的函数调用和分支转移有了准确的地址偏移,以便于接下来链接器进行的链接过程。

5章 链接

5.1 链接的概念与作用

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

作用:链接过程使得分离编译(separate compilation)成为可能,它使得程序员编程时不必将大型的应用程序组织为一个巨大的源文件,而是可以将其分解为更小、更好管理的模块,可以独立地修改和编译这些模块,当修改其中一个模块时,只需要重新编译该模块并重新链接应用,而不必编译其它文件,链接对模块化编程起到了重要的作用。

注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

命令:

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

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

Hello(可执行文件)的ELF与hello.o的ELF都包含ELF 头,程序头表,.text 节,.rodata节,.bss节,.symtab节,.debug节,节头表。但是可执行文件里少了.rel.txt节和.rel.data节,因为在链接的过程中他们的任务已经完成了故生成的可执行文件里不再包含.rel.txt节和.rel.data节。但是新增了和动态链接库相关的重定位信息.rela.dyn ,同时,新增了.init节各段的起始地址,大小等信息可以在节头部表里查看。

5.4 hello的虚拟地址空间

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

在edb中打开可执行文件hello,可以看到hello可执行部分(代码段)起始地址为0x401000,结束地址为0x401ff0

在5.3中,可以看到,.init节的起始地址是0x401000,二者之间是对应的

5.5 链接的重定位过程分析

命令:objdump -d -r hello > hello.ss

  1. 链接后函数数量增加

链接后的反汇编文件hello.ss多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。原因是动态链接器将共享库中hello.c用到的函数加入可执行文件中。

 2.函数调用指令call的参数发生变化

链接器在链接过程中,处理了重定位条目,将call指令后的字节码修改为目标地址与下一条指令地址的差值,从而使call指令能够跳转到正确的代码段,生成了完整的反汇编代码。

3.跳转指令参数发生变化

 在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。

5.6 hello的执行流程

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

利用edb单步调试,每次跳转的函数依次记录

_start:0x00000000004010f0

__libc_start_main:0x00007fa82ac29dc0

__cxa_atexit:0x00007f57014458c0

_init:0x0000000000401000

puts@plt:0x0000000000401090

printf@plt:0x00000000004010a0

getchar@plt:0x00000000004010b0

atoi@plt:0x00000000004010c0

exit@plt:0x00000000004010d0

sleep@plt:0x00000000004010e0

deregister_tm_clones:0x0000000000401130

_dl_relocate_static_pie:0x0000000000401120

register_tm_clones:0x0000000000401160

__do_global_dtors_aux:0x00000000004011a0

frame_dummy:0x00000000004011d0

_fini:0x0000000000401270

5.7 Hello的动态链接分析

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

  在hello程序中可以发现调用共享库的函数名称后面都加了“@plt”,通过观察反汇编文件,可以发现使用这些函数时都会跳转到.plt节中,如图34所示,hello程序调用了6个共享库的函数,.plt节中就有6处跳转指令,它们跳转到了同一个地址0x401020,然后在0x401026处它们会进行一个间接跳转,跳转到<_GLOBAL_OFFSET_TABLE_+0x10>处,及0x404120内存中存放的地址处。

而这个地址在dl_init前,这个值如图左侧所示,是一个空值,而当调用一个动态链接库中的函数时,该地址内容值会发生变化,使其跳转到所调用的对应的函数地址处。

5.8 本章小结                               

本章介绍了链接的概念和作用,以及动态链接的原理。通过使用edb、gdb、objdump等工具,分析了hello程序的虚拟地址空间、ELF文件格式、重定位和执行过程,深入理解了链接的各个环节。

6章 hello进程管理

6.1 进程的概念与作用

概念:进程的定义是计算机一个执行中程序的实例,也称为是计算机中的程序关于某数据集合上的一次运行活动。它是系统进行资源分配的基本单位,是操作系统结构的基础,系统中的每个程序都运行在某个进程的上下文中。

作用:进程是对正在运行的程序过程的抽象,进程可以抽象地提供给应用程序一个独立的逻辑控制流和一个私有的地址空间。它可以清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。

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

作用:Shell是一种命令行解释器, 其读取用户输入的字符串命令, 解释并且执行命令. 它是一种特殊的应用程序, 介于系统调用/库与应用程序之间, 其提供了运行其他程序的的接口.它可以是交互式的, 即读取用户输入的字符串;也可以是非交互式的, 即读取脚本文件并解释执行, 直至文件结束。

处理流程:当Shell接受一条命令后,他会首先解析命令行中的命令及相关参数,如果命令是要执行一个程序,它会首先执行fork函数创建一个新的子进程,然后在这个子进程中执行execve函数在加载并运行这个程序,之后就会在前台进程中执行waitpid函数等待子进程中程序执行完毕后回收该子进程。如果命令是一个内置的shell命令,它就直接执行该命令。

6.3 Hello的fork进程创建过程

当父进程调用fork函数可创建一个新的子进程。新创建的子进程得到与父进程用户级虚拟地址空间相同(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。父进程与子进程之间最大的区别在于它们有不同的PID,父进程中fork返回子进程的PID,子进程的PID总是非0,而子进程中fork返回0。

 以运行hello程序为例,当输入./hello时,父进程为shell,它会对这条命令进行解析,因为这不是内置shell命令,它判定要执行hello这个可执行文件,于是它就调用fork函数创建一个新的子进程以便接下来将hello加载到这个进程中执行。

6.4 Hello的execve过程

execve函数声明为:int execve(char *filename,char *argv[],char *envp[])

execve函数会在当前进程的上下文中加载并运行一个新程序,它加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp,在execve加载filename时会调用一个加载器,加载器会创建内存映像并将可执行文件的片(chunk)复制到代码段和数据段,接下来,加载器会跳转到程序的入口,即_start函数的地址点来设置用户栈,如图36所示。初始化程序后就会把控制权传递给main函数。

以运行hello程序为例,当shell调用fork函数创建一个新的子进程后,它会调用execve函数加载并运行可执行目标文件hello,如果命令./hello后跟有参数,shell也会把这些参数当作参数列表argv一起传入进程,这样hello就实现了由程序到进程的转变,之后完成初始化程序后,就会正式运行main函数。

6.5 Hello的进程执行

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

一个进程和其它进程轮流进行的概念称为多任务,一个进程执行它的控制流的一部分的每一时间段叫做该进程的时间片,操作系统内核使用一种称为上下文切换的异常控制流实现多任务,具体如图37所示。

内核为每个进程维持一个上下文,系统中的每个程序都运行在某个进程的上下文中,进程上下文信息就是内核重新启动一个被抢占的进程所需的状态,它由一些对象的值组成,包括寄存器、程序计数器、用户栈等。

以执行程序hello中的sleep函数为例,当main函数执行了sleep系统调用函数时,触发了陷阱异常,此时从用户模式切换为内核模式,main所在进程休眠一段时间,控制权交给其它进程,执行了上下文切换,此时会切换回用户模式。当休眠结束时,会发送信号给内核,此时又会进入异常处理程序,又从用户模式切换为内核模式,它会执行从其它进程到main所在进程的上下文切换,结果控制权又交回给main所在进程。调用其它系统函数时也会有类似的进程调度过程。

6.6 hello的异常与信号处理

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

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

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

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

hello执行过程中出现的异常种类可能会有:中断、陷阱、故障、终止。

中断:中断是来自处理器外部的I/O设备的信号的结果。如在按下ctrl-z后,会触发一个中断异常。

陷阱:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。

故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则处理程序返回到内核中的abort例程并将其终止。

终止:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。

6.7本章小结

在使用shell执行hello的过程中,hello从可执行文件加载到内存中真正开始运行。Shell调用fork和execve创建一个新进程并在它的上下文里加载hello。Hello在运行中可能会收到来自键盘的信号,hello进程对于这些信号会做出相应的反应

7章 hello的存储管理

7.1 hello的存储器地址空间

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

逻辑地址是指在hello反汇编代码中可见的地址,它由一个段标识符和一个段内偏移量组成,用于定位操作数或指令的位置。

线性地址是逻辑地址向物理地址转换过程中的中间结果,它是通过段机制将逻辑地址转换为一维地址空间的地址。在保护模式下,虚拟地址也可以用“段:偏移量”的形式表示,其中的段是指段选择器。Hello反汇编的地址即为虚拟地址。

虚拟地址是保护模式下程序访问存储器所使用的逻辑地址,它可以通过分页机制映射到不同的物理地址。

物理地址是加载到内存地址寄存器中的地址,它是内存单元的实际地址。CPU通过地址总线寻址时,使用的是物理地址。在前端总线上传输的内存地址也都是物理地址。

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

段式存储管理是一种以段为单位进行内存分配和回收的方法,它根据程序的逻辑结构将程序划分为若干个段,每个段具有一个段名和一个段内偏移量,用于定位程序或数据的位置。每个段在内存中占据一个连续的空间,但不同段之间可以不连续。段式存储管理需要维护一个段表,记录每个段的基址、长度、存取控制等信息。地址变换时,通过查找段表,将逻辑地址转换为物理地址。

段式管理的优点是能够实现程序的动态链接、保护和共享。动态链接是指在程序运行过程中,根据需要动态地装入或卸载某些段,实现程序的模块化。保护是指通过设置每个段的存取控制位,防止非法访问或修改。共享是指允许多个进程共同使用某些公共的程序或数据段,节省内存空间。

线性地址是逻辑地址向物理地址转换过程中的中间结果,它是通过将逻辑地址中的段内偏移量加上相应的段基址得到的。线性地址可以看作是一维的虚拟地址空间,它可以进一步通过分页机制映射到物理地址空间。

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

为了实现线性地址到物理地址的转换,操作系统采用了分页机制,将线性地址空间划分为等长的单元,称为页。由于页表可能占用大量的内存空间,操作系统使用了两级页表结构,即页目录表和页表。页目录表的物理地址保存在cr3寄存器中。

当处理器访问一个线性地址时,它会通过内存管理单元(MMU)生成一个虚拟地址,并根据虚拟地址的高10位在页目录表中查找对应的页表地址。然后,它会根据虚拟地址的中间10位在页表中查找对应的页帧地址。最后,它会将页帧地址和虚拟地址的低10位相加,得到最终的物理地址。

在这个过程中,处理器需要向高速缓存或主存发出请求,获取页目录表项和页表项。如果这些项已经存在于高速缓存中,那么处理器可以快速地得到物理地址。如果这些项不存在于高速缓存中,那么处理器需要从主存中读取它们,并将它们缓存在高速缓存中,以便下次使用。

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

TLB是一个存储单个PTE的块的小型虚拟地址缓存,每个块对应一个虚拟页。当MMU接收到虚拟地址时,它会在TLB中查找匹配的块。如果找到,它会从PTE中获取物理页号,并与虚拟页偏移量组合成52位的物理地址。

如果TLB未命中,MMU需要访问四级页表来获取PTE。CR3寄存器指向第一级页表的基址。虚拟页号的第一部分作为第一级页表的索引,得到一个PTE。如果该PTE有效且权限正确,它会指向第二级页表的基址。同样地,虚拟页号的第二、第三和第四部分分别作为第二、第三和第四级页表的索引,得到最终的PTE。该PTE包含物理页号,与虚拟页偏移量组合成物理地址,并添加到TLB中。

如果在访问任何级别的页表时发现PTE无效或权限错误,MMU会触发缺页中断。

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

采用CT、CI、CO三个字段对物理地址进行分割,并按照以下步骤实现cache的访问和替换。首先,根据CI字段在一级cache中定位相应的组,并检查该组中所有标志位为有效的块的CT字段是否与物理地址的CT字段相匹配。如果匹配成功,则表示一级cache命中,并返回相应的数据块。如果匹配失败,则表示一级cache不命中,需要继续在二级cache中查找。同样地,根据CI字段在二级cache中定位相应的组,并检查该组中所有标志位为有效的块的CT字段是否与物理地址的CT字段相匹配。如果匹配成功,则表示二级cache命中,并将该数据块复制到一级cache中,并返回数据块。如果匹配失败,则表示二级cache不命中,需要继续在三级cache中查找。依此类推,如果三级cache也不命中,则需要从主存中读取数据块,并将其复制到各级cache中,并返回数据块。如果主存发生缺页中断,则需要从硬盘中读取数据块,并将其复制到主存和各级cache中,并返回数据块。

7.6 hello进程fork时的内存映射

shell执行fork函数时,内核为hello进程分配了唯一的PID,并建立了相应的数据结构和task_struct,同时创建了独立的虚拟地址空间。为了实现虚拟内存,内核复制了当前进程的mm_struct、区域结构和页表。它将两个进程共享的页面都设为只读,并将两个进程的区域结构都设为私有的写时复制。这样,fork在新进程返回时,新进程的虚拟内存与fork调用时的虚拟内存完全一致。当两个进程中任意一个进行写操作时,写时复制机制会生成新页面,从而保证了每个进程都拥有私有的地址空间抽象

7.7 hello进程execve时的内存映射

在加载新程序时,操作系统需要对当前进程的用户虚拟地址空间进行重新映射。首先,它会删除已存在的用户区域结构,释放原来的代码、数据、bss、栈和堆区域所占用的物理内存。然后,它会为新程序的代码、数据、bss和栈区域创建新的用户区域结构,并将它们映射到相应的文件或匿名文件中。这些新的区域都是私有的,并且采用写时复制的策略。接着,它会检查新程序是否与共享对象链接,如果是的话,它会动态地将这些对象映射到用户虚拟地址空间中的共享区域中。最后,它会设置程序计数器(PC),使其指向新程序的代码区域的入口点。

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

缺页故障:当CPU访问虚拟内存中的某个地址,而该地址对应的物理页已经加载在主存中时,称为页命中。反之,如果主存中没有该物理页,称为缺页。

缺页中断处理:当发生缺页时,CPU会调用缺页异常处理程序,该程序负责从主存中选择一个可替换的物理页,称为牺牲页。如果牺牲页已经被修改过,那么需要将其写回磁盘。然后,从磁盘中读取所需的物理页,并将其放入牺牲页的位置。同时,更新相关的PTE,使其指向新的物理页。最后,恢复引起缺页的指令,并让CPU重新发送虚拟地址给MMU。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

动态内存管理是系统软件的基本组件之一,它负责在堆上为应用程序动态地分配和回收内存空间。动态内存分配器通过维护堆上的不同大小的内存块来实现动态内存管理,其中每个内存块都是一段连续的虚拟地址空间,可以是已经被分配给应用程序的或者是尚未被使用的。当应用程序请求一定大小的内存时,动态内存分配器需要从空闲块中选择一个合适的块来满足请求,并将其标记为已分配状态。当应用程序释放已分配的内存时,动态内存分配器需要将其恢复为空闲状态,并尝试与相邻的空闲块进行合并,以减少内存碎片。

为了高效地管理堆上的内存块,动态内存分配器通常采用以下三种策略:

记录空闲块策略:这种策略决定了如何组织和查找空闲块,以便快速地找到合适的空闲块来满足应用程序的请求。常见的记录空闲块策略有显示空闲链表、隐式空闲链表、分离空闲链表和红黑树等。

放置策略:这种策略决定了如何从找到的空闲块中划分出所需大小的内存给应用程序,并处理剩余部分。常见的放置策略有首次适配、下一次适配和最佳适配等。

合并策略:这种策略决定了何时以及如何将相邻的空闲块进行合并,以避免或减少内存碎片。常见的合并策略有立即合并和延迟合并等。

7.10本章小结

本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理的相关内容,对hello的存储管理有了较为深入的讲解。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

Linux内核采用了一种优雅的设备抽象方式,即将所有的I/O设备(如网络、磁盘和终端)视为文件,并将所有的输入和输出操作统一为对应文件的读写操作。这样,Linux内核就可以提供一个简单而低级的接口,即Unix I/O,实现了输入和输出操作的一致性和统一性。

8.2 简述Unix IO接口及其函数

接口:

(1)打开文件:程序通过调用open()函数来请求内核分配一个可用的描述符,并将其与指定路径名的文件关联起来。描述符是一个非负整数,可以唯一地标识进程打开的某个文件。内核维护了一个打开文件表,记录了每个打开文件的相关信息,如访问权限、当前位置、引用计数等。程序在后续对该文件的操作中只需使用该描述符即可。

(2)linux shell创建的每个进程都有三个预定义的描述符:标准输入、标准输出和标准错误。它们分别对应着0、1和2这三个常量值,可以在头文件中找到它们的定义。它们分别表示进程从终端读取输入、向终端输出结果和向终端报告错误信息所使用的描述符。

(3)改变当前的文件位置:对于每个打开的文件,内核都会记录其当前位置,即从该文件起始处开始计算的字节偏移量。程序可以通过调用lseek()函数来显式地改变某个描述符所对应的当前位置,以便在随机访问模式下读写该文件。

(4)读写文件。读操作就是通过调用read()函数来将指定描述符所对应的当前位置开始的多个字节复制到内存缓冲区中。如果在给定大小范围内执行读操作时遇到了该文件的末尾,则会产生一个称为EOF(end-of-file)的条件,并返回实际读取到的字节数。应用程序可以通过检测这个条件来判断是否已经读完了该文件。类似地,写操作就是通过调用write()函数来将内存缓冲区中的多个字节复制到指定描述符所对应的当前位置开始的那些字节上,并更新当前位置。

(5)关闭文件。当程序不再需要访问某个打开的文件时,它可以通过调用close()函数来通知内核释放该描述符,并更新打开文件表中相应条目的信息。作为响应,内核会减少该条目的引用计数,并在引用计数为零时将其删除。当进程因为任何原因终止时,内核也会自动关闭该进程打开的所有文件,并释放相关资源。

函数:

(1)open()函数用于打开或创建文件。

(2)close()函数用于关闭一个被打开的的文件。

(3)read()函数用于从文件读取数据。

(4)write()函数用于向文件写入数据。

(5)lseek()函数用于在指定的文件描述符中将将文件指针定位到相应位置。

8.3 printf的实现分析

printf函数的函数体如图52所示。

首先printf函数输入的参数中含有“…”,这代表可变形参,即传递参数的个数不确定。

之后va_list arg = (va_list)((char*)(&fmt) + 4)中va_list为一个定义的字符指针类型,arg即为一个字符指针,而fmt是一个指针,这个指针指向第一个const参数中的第一个元素。那么清楚arg即为printf可变形参中的第一个参数的地址。

vsprintf的定义为:int vsprintf(char *buf,const char *fmt,va_list args)。这命令的目的是将从第一个传递参数开始,依次把这个字符串传入buf缓冲区中,其中返回值为i即是要打印出来的字符串的长度。

最后write函数就会把buf中的i个元素的值写到终端。

但从硬件上看,从vsprintf到最终显示器上显示出字符串还有对应漫长过程:

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。接着字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。最后显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

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

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

当用键盘输入字符串时,会触发键盘中断异常,执行键盘中断处理子程序。字符接受按键扫描码转化成ASCII码,保存到系统的键盘缓冲区,getchar调用read函数,将键盘缓冲区中的字符读入buf中,并测得该字符串的长度为n,然后令字符指针bb指向buf。最后返回buf中第一个字符。如果长度n<0,则会报EOF错误。

8.5本章小结

本章主要介绍了Linux系统中的I/O设备管理和Unix I/O接口,以及它们在实现基本的输入输出功能中的作用。窗体顶端

结论

编写一个输出“Hello, world!”的hello.c源文件

使用gcc命令对hello.c进行预处理、编译、汇编和链接,生成可执行程序hello

使用./hello命令运行可执行程序hello

shell进程调用fork()函数创建一个子进程,并复制父进程的地址空间和状态

子进程调用execve()函数加载可执行程序到虚拟内存,并覆盖原来的地址空间和状态

MMU负责将程序中的虚拟地址转换为物理地址,并检查地址是否合法和可访问

处理器根据程序中的指令执行逻辑控制流,包括顺序执行、条件分支、循环等

程序正常结束或者出现异常时,调用信号处理函数进行处理,并返回相应的退出码

进程结束后,操作系统负责释放和回收其占用的资源,包括内存、文件描述符等

计算机系统的设计非常精妙,实现这整个系统每个环节环环相扣,缺一不可。

计算机系统的设计是一个高度复杂且精细的工程,它涉及到编程、编译、进程管理、内存管理、程序加载、异常处理等多个方面。这些部分相互依赖、相互协作,共同构成了一个功能强大、稳定可靠的计算机系统。

附件

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

hello.c源程序

hello.i hello.c预处理后的预编译处理文件

hello.s hello.i编译后的汇编文件

hello.o hello.s汇编后的可重定位目标文件

hello hello.o链接后的可执行目标文件

dump.txt hello.o反汇编后文件

hello.ss hello反汇编后文件

hello.elf hello.o的ELF格式文件

hello.elf hello的ELF格式文件

printf.txt print函数的函数体

getchar.txt getchar函数的函数体

参考文献

[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]  《深入理解计算机系统》Randal E. Bryant David R.O`Hallaron

[8]  printf函数实现的深入剖析.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值