哈工大计算机系统大作业(2023秋)——程序人生-Hello’s P2P ...

打印 上一主题 下一主题

主题 511|帖子 511|积分 1533

摘  要

本论文研究了hello程序在Linux系统下的整个生命周期。从源代码hello.c开始,依次深入实践了预处理、编译、汇编、链接、运行、回收的完备过程,从而对hello.c文件的“一生”有了更具体的熟悉。本论文以hello.c文件为研究对象,联合《深入明白计算机系统》书中的内容与讲堂内容,在Ubuntu系统下对hello程序的整个生命周期进行了研究,同时梳理回顾了学习内容,加深了对计算机系统的了解。
关键词:计算机系统;程序的生命周期;编译过程;深入明白计算机系统


第1章 概述

1.1 Hello简介

此程序为一个延时打印“Hello+姓名+学号”的程序。
P2P:程序通过程序员的编码形成程序代码文件(Program),颠末预处理、编译、汇编、链接等一系列过程,得到二进制程序文件,从而将其变成了一个运行的进程(Process)。这就是程序的P2P。
020:程序一开始不在内存空间中,OSfork出一个子进程,然后execve执行hello程序,OS会为他开一块假造内存,同时将程序加载到假造内存映射到的物理内存中。当程序执行完,OS回收这一程序,同时为该程序的开发的内存空间也会被回收,此时又变为0。这就是程序的020。
1.2 环境与工具

硬件环境:Intel x64 CPU 2.50GHz
软件环境:Windows 11 64位; VirtualBox; Ubuntu 22.04 LTS 64位
工具:编辑器VS Code、反汇编工具EDB、编译环境GCC
1.3 中间效果

列出你为编写本论文,天生的中间效果文件的名字,文件的作用等。
文件名
作用
hello.c
源代码
hello.i
预处理后得到的文本文件
hello.s
编译后得到的汇编语言文件
hello.o
汇编后得到的可重定位目的文件
hello_dump.s
反汇编hello.o得到的反汇编文件
hello_o.elf
用readelf读取hello.o得到的ELF格式信息
hello
可执行文件
hello_exec.s
反汇编hello可执行文件得到的反汇编文件
hello.elf
由hello可执行文件天生的.elf文件
表 1 中间效果

1.4 本章小结

本章重要先容了hello.c P2P和020的过程。列出了本次实验所需的环境和工具以及过程中所天生的中间效果。
第2章 预处理

2.1 预处理的概念与作用

预处理即在编译之前对代码进行的处理,为后续的编译工作带来便利。 C语言的预处理重要有以下几个方面的内容:

  • 宏定义替换:如#define,将宏展开为实际的代码片断。
  • 文件包含:如#include,将指定的头文件内容放入源文件中。
  • 条件编译:如#if、#endif、#elif等,根据条件删除或包含部分代码块。
  • 解释:会将解释删除。
  • 其他预编译器提示字:如#error,确保特定的编译选项已经被定义。
2.2在Ubuntu下预处理的命令

使用gcc -E hello.c -o hello.i或cpp hello.c -o hello.i都可以对hello.c进行预处理。
根据作业要求,此处使用gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i ,带参数进行预处理。

图 1 预处理

2.3 Hello的预处理效果解析

颠末预处理后,文件显着变长,由原本的25行变长至3093行。原本的代码位于预处理文件的末端。

图 2 预处理文件中的原代码

预编译文件前面的部分对包含的头文件进行进行包含,依次是stdio.h、unistd.h、stdlib.h。

图 3 对stdio.h的包含(部分)


图 4 对unistd.h的包含(部分)


图 5 对stdlib.h的包含

此外,原本代码文件中的解释被删去了,预处剖析忽略C代码文件当中的解释。
2.4 本章小结

本章先容了预处理的概念和作用,尝试对代码进行了预处理,分析了代码预处理的效果,对预处理过程有了更加深入的熟悉。
第3章 编译

3.1 编译的概念与作用

编译是指使用编译器将预处理后的代码文件转换为汇编语言文件的过程。
3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s可以实现对预处理文件的编译。根据作业要求,使用gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s对预处理文件进行编译。

图 6 对预处理文件进行编译

编译得到汇编语言文件hello.s
3.3 Hello的编译效果解析

3.3.1数据和赋值操纵

(1)常量:
对于if或for语句中的立即数,他们的值直接保存在.text中。

图 7 if中的常量储存


图 8 for中的常量储存

对于printf中字符串类型的常量,他们被保存在.rodata中

图 9 字符串型常量

(2)变量:
程序若有已初始化的全局变量,会存储在.data中。
1.变量i:


图 10 变量i的赋值

在进入循环前将i赋值为0,可以看到i存储的位置是%rbp-4,保存在栈中。
2.变量argc与*argv[]。

图 11 argc和*argv[]的存储位置

argc存储在%rbp-20;*argv[]存储在%rbp-32。
(3)赋值:赋值使用mov指令,根据数据的巨细决定使用哪一条赋值语句。
指令
movb

movw

movl

movq

数据巨细(字节)
1

2

4

8

表 2 mov的类型

3.3.2算术操纵


图 12 addl指令进行算术操纵

在此程序中,每一轮循环竣事后,i加上1。
3.3.3数组/指针/结构操纵

用户所输入的参数存储在*argv[]指向的字符串数组中,由上文知*argv[]存储在%rbp-32。

图 13 数组操纵

可以观察到argv[1]位于(%rbp-32)+8; argv[2]位于(%rbp-32)+16; argv[2]位于(%rbp-32)+24。
3.3.4关系操纵

使用cmp比较两个值的关系,将效果存放到条件码寄存器中。

图 14 判定argc!=4


图 15 判定i<8

3.3.5控制转移


图 16 if判定argc!=4

je用于判定cmpl比较的效果,若两个操纵数的值不相称则跳转到指定地址。

图 17 for循环

For循环首先初始化i的值,之后进行循环条件判定,使用cmpl和jle进行分支操纵,假如i小于等于7则可以继承循环,否则不能。每次循环体内代码执行完毕后,i自加1。
3.3.6函数操纵

函数操纵首先需要为参数赋值,然后调用参数。

图 18 寄存器表

(1)main函数:
参数传递:传入参数argc和*rgv[],分别用寄存器%rdi和%rsi存储。

函数调用:被系统启动函数调用。

函数返回:设置%eax为0而且返回,对应return 0。


图 19 main函数的参数获取


图 20 设置返回值为0

(2)printf函数
参数传递:call puts只传入字符串首地址置于%rdi;call printf中,字符串首地址置于%rdi,argv[1],argv[2]分别放在%rsi、%rdx。
函数调用:call puts;call printf。

图 21 call puts


图 22 call printf

(3)exit函数

参数传递:将1置于%rdi。

函数调用:call exit


图 23 call exit

(4)atoi函数
参数传递:将argv[3]置于%rdi。
函数调用:call atoi
函数返回:返回值存储在%eax

图 24 call atoi

(5)sleep函数
参数传递:将atoi的返回值置于%rdi。
函数调用:call sleep

图 25 call sleep

(6)getchar函数
函数调用:call getchar

图 26 call getchar

3.4 本章小结

本章先容了编译的概念和作用,尝试对预处理后的代码进行了编译,分析了编译的效果,对编译过程和汇编语言有了更加深入的熟悉。

第4章 汇编

4.1 汇编的概念与作用

汇编是使用汇编器将汇编语言代码转换成呆板语言目的文件的工具。它读取汇编语言源文件,将每条汇编指令翻译成对应的呆板指令,并天生可重定位目的文件,目的文件中包含了呆板指令的二进制表示以及其他相关的信息。为下一步进行链接准备条件。
4.2 在Ubuntu下汇编的命令

as hello.s -o hello.o和gcc -c hello.s -o hello.o都可以实现对hello.s的汇编。
根据作业要求,使用 gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o进行汇编。

图 27 汇编

4.3 可重定位目的elf格式

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

  • ELF头:ELF头记录了整个ELF文件的基本信息,包含其种别,目的体系结构等。

图 28 ELF头


  • 节头:ELF 文件中,每个节都有一个对应的节头表,用于描述和定位各个节的信息,通过节头表,可以获取关于每个节的具体信息,如名称、偏移、巨细等。

图 29 节头


  • 重定位节:重定位节记录了需要在链接时修正的位置信息,它包含了与链接器相关的信息,以便在最终可执行文件中正确地安排符号的地址。

图 30 重定位节


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

图 31 符号表

4.4 Hello.o的效果解析

4.4.1 数字进制表示不同
       hello.s中数字用10进制表示,反汇编中数字用16进制表示。

图 32 数字进制不同

4.4.2 分支转移
       hello.s中的分支转移中用段的名字表示跳转目的,因为hello.o已经是可重定位文件,反汇编文件中每一行已经分配了地址,跳转命令后接的是跳转的目的地址

图 33 分支转移不同

4.4.3 函数调用
Hello.s中call后接的是函数名称,而反汇编文件中接的是相对main函数的偏移地址,同时在反汇编代码中调用函数的操纵数都为0,这是因为在链接后才会天生确定的地址,故此处暂时用0填充。

图 34 函数调用不同

4.5 本章小结

本章先容了汇编的概念和作用。颠末汇编器之后,天生了一个可重定位的文件hello.o,为下一步链接做好了准备。通过hello.o的反汇编代码与hello.s的比较,对汇编作用的明白更加深刻。

第5章 链接

5.1 链接的概念与作用

链接是将各种不同文件的代码和数据片断收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。
其作用在于把预编译好了的多少目的文件合并成为一个可执行目的文件。使得分离编译称为可能,不用将一个大型的应用程序构造为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。当改变这些模块中的一个时,只需简单重新编译它并重新链接即可,不必重新编译其他文件。
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 -no-pie


图 35 链接

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

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

  • ELF头:ELF头记录了整个ELF文件的基本信息,包含其种别,目的体系结构等。

图 36 ELF头


  • 节头:ELF 文件中,每个节都有一个对应的节头表,用于描述和定位各个节的信息,通过节头表,可以获取关于每个节的具体信息,如名称、偏移、巨细等。

图 37 节头


  • 程序头:用于描述如何将文件的各个段加载到内存中。每个程序头表项提供了有关一个段在内存中的布局和属性的信息,以便操纵系统的加载器正确加载可执行文件。

图 38 程序头


  • 动态区段:用于存储程序在运行时所需的动态链接信息。它包含一系列的动态入口,这些入口提供了程序在运行时获取共享库和其他动态链接信息的方式。

图 39 动态区段


  • 重定位节:用于存储关于程序中需要在运行时进行地址重定位的信息。重定位节包含了对符号的引用以及如何修改这些引用的信息,以便程序在加载到内存时能够正确执行。

图 40 重定位节


  • 符号表:用于存储程序中使用的符号信息。符号表记录了程序中定义和引用的变量、函数以及其他符号的相关信息,包括符号的名称、类型、巨细、地址等。

图 41 符号表

5.4 hello的假造地址空间

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

图 42 假造内存地区表

可以根据节头表查看各段的起始位置,如对于.rodata段,地址为0x0000000000402000。

图 43 节头表(部分)

在edb中转到相应地址,可以看到假造地址空间中的内容。

图 44 EDB查找.rodata

其余各段以此类推。
5.5 链接的重定位过程分析


图 45 反汇编hello

(1)Hello的反汇编相较于hello.o的反汇编,每行指令都有唯一的假造地址,这是因为hello颠末链接,已经完成重定位,每条指令的地址关系已经确定。
(2)扩充了许多函数代码,增加了.init段和.plt段,包括程序加载后执行main前的一些准备工作,以及hello需要用到的一些库函数的定义,这是因为动态链接器将共享库中hello.c用到的函数参加可执行文件中。
(3)原本在hello.o中等待重定位而暂时置0的地址操纵数,成功进行了重定位,并计算了偏移量,被设置为了假造地址空间中的地址。

图 46 链接前后反汇编代码的比较

链接器在重定位步调中,合并输入模块并将运行时地址赋给输入模块定义的每个节、符号。当这一步完成时,程序中的每条指令和全局变量才拥有唯一的运行时内存地址。
5.6 hello的执行流程

在正确输入的情况下hello会依照以下顺序调用模块
地址
子程序名
0x0000004010f0
_start
0x7f8d32029dc0
__libc_start_main
0x000000401000
_init
0x000000401125
main
0x000000401040
printf@plt
0x000000401060
atoi@plt
0x000000401080
sleep@plt
0x000000401050
getchar@plt
表 3 hello的执行顺序


图 47 使用分析模块调用

5.7 Hello的动态链接分析

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到恣意位置。正常的方法是为该引用天生一条重定位记录,然后动态链接器在程序加载的时间再解析它。GNU编译系统使用耽误绑定,将过程地址的绑定推迟到第一次调用该过程时。耽误绑定是通过GOT和PLT实现的。根据ELF文件,可知GOT节和PLT节的起始地址。

图 48 GOT节和PLT节的起始地址


图 49 GOT与PLT的作用过程

执行_init前:

图 50 执行_init前的内容

执行_init后:

图 51 执行_init后的内容

5.8 本章小结

本章中先容了链接的概念与作用。观察了hello文件ELF格式下的内容。使用edb观察了hello的假造地址空间使用情况并以hello为例对重定位过程、执行过程和动态链接过程进行分析。


第6章 hello进程管理

6.1 进程的概念与作用

(1)进程的概念:
狭义定义:进程是正在运行的程序的实例
广义定义:进程是一个具有一定独立功能的程序关于某个数据聚集的一次运行运动。它是操纵系统动态执行的基本单元,在传统的操纵系统中,进程既是基本的分配单元,也是基本的执行单元。
(2)进程的作用:
进程是对正在运行的程序过程的抽象;实现角度上,进程是一种数据结构,目的在于清楚地描画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
6.2 简述壳Shell-bash的作用与处理流程

寄义:Shell是操纵系统的最外层,是一个用户跟操纵系统之间交互的命令解释器,让用户能够更加高效、安全、低本钱地使用 Linux 内核。
作用:Shell 应用程序提供了一个界面,用户通过这个界面访问操纵系统内核的服务。
处理流程:
(1)从终端读入输入的命令;
(2)将输入字符串切分获得所有的参数;
(3)假如是内置命令则立即执行;
(4)否则调用相应的程序执行;
(5)shell 应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程

当Hello父进程调用fork 时,操纵系统会复制当前进程的副本,包括代码、数据和堆栈等,从而天生一个新的子进程。这两个进程险些是相同的,但它们有着不同的PID。父子进程之间的执行是并发的,它们在fork调用之后分别继承执行。fork的返回值在父进程中是子进程的PID,而在子进程中是0,这样可以通过返回值的不同来区分执行流。因为进程的地址空间是独立的,之后父子进程可以独立地执行各自的任务,互不干扰。
6.4 Hello的execve过程

execve 是一个在 Unix 系统中的系统调用,用于在当前进程中加载并执行一个新调用execve时,操纵系统会用指定的可执行文件替换当前进程的地址空间,包括代码、数据和堆栈等。新程序的执行从其main函数开始,完全取代了原始进程的执行。这个过程包括(1)打开指定的可执行文件;(2)加载其代码和数据到内存;(3)设置新程序的堆栈和参数;(4)最终将控制权转交给新程序。
6.5 Hello的进程执行

6.5.1 相关信息
(1)进程上下文信息:进程上下文信息包括了进程的状态、寄存器值、程序计数器等。当操纵系统决定切换到另一个进程时,它需要保存当前进程的上下文信息,以便稍后能够恢复到该进程的执行状态。

图 52 使用系统调用时的上下文切换

(2)用户态与焦点态:大多数操纵系统将处理器的执行分为用户态和焦点态。在用户态下,进程只能执行受限的指令集,而在焦点态下,进程可以执行更多的特权指令。进程从用户态切换到焦点态需要进行特权级的切换,通常通过系统调用或异常来实现。
(3)每个进程在系统中分配到的执行时间被称为时间片。当一个进程的时间片用尽时,操纵系统可以选择切换到另一个进程,以便为其他进程提供执行机会。时间片轮转是一种常见的调度算法,确保每个进程都有机会执行。
6.5.2 进程调度的过程
(1)停当队列:操纵系统维护一个停当队列,其中包含了所有准备好被执行的进程。这些进程通常已经加载到内存中,但由于某些原因而暂时没有执行。
(2)选择下一个执行的进程:调度器根据调度算法(如时间片轮转)从停当队列中选择下一个要执行的进程。
(3)保存当前进程的上下文:假如当前有正在执行的进程,其上下文信息将被保存,以便稍后能够继承执行。
(4)切换到选定进程的上下文:调度器加载下一个进程的上下文信息,将控制权转移到该进程。
(5)执行进程:被选中的进程开始执行,运行一段时间,直到它主动开释 CPU,或者它的时间片用尽,或者它被更高优先级的进程抢占。
(6)循环:这个过程不绝循环,根据调度算法选择下一个执行的进程,切换上下文,执行进程,直到所有进程完成执行。
6.5.3 用户态与焦点态转换
(1)用户态到焦点态的转换:当进程需要执行特权操纵(比方访问硬件设备、进行文件操纵、执行系统调用等)时,它必须切换到焦点态。这通常通过系统调用触发。在这种情况下,操纵系统会保存当前进程的用户态上下文,切换到焦点态,并执行相应的内核代码。
(2)焦点态到用户态的转换:一旦焦点态的工作完成,操纵系统将恢复先前保存的用户态上下文,将控制权返回给用户态的进程。这个过程确保了用户程序无法直接操纵系统内核,同时保护了系统的稳定性和安全性。
6.6 hello的异常与信号处理

(1)hello执行过程中可能出现的异常、产生的信号和处理方式。

图 53 异常列表

Linux定义的信号重要有如下几种:

图 54 Linux内审定义的信号

而在Hello程序的执行过程中比较可能出现的重要有SIGINT、SIGQUIT、SIGCHLD、SIGSEGV、SIGALRM、SIGTSTP、SIGKILL、SIGTERM、SIGCONT等几种。
程序对异常的处理方法如下图所示:

图 55 中断处理方式


图 56 陷阱处理方式


图 57 故障处理方式


图 58 停止处理方式

(2)各种状况下的运行效果
1. 正常运行:


图 59 正常运行

2. 不绝乱按:不影响程序正常输出


图 60 不绝乱按时输出

3.按回车:不影响程序正常工作,但回车会填充进输入缓冲区

图 61 按回车时输出

4. 按Ctrl+Z:发送SIGTSTP信号,进程停止

图 62 按下Ctrl+Z

5. 按Ctrl+C:发送SIGINT信号,进程停止

图 63 按下Ctrl+C

(3)Ctrl+Z后运行命令
1. ps命令:

图 64 Ctrl+Z后使用ps

2.jobs命令:

图 65 Ctrl+Z后使用jobs

3.pstree命令:

图 66 Ctrl+Z后使用pstree

4.fg命令:程序回到前台继承运行

图 67 Ctrl+Z后使用fg

5.kill命令(默认发送SIGTERM):

图 68 kill不带参数(SIGTERM)


图 69 指定发送SIGKILL

6.7本章小结

本章重要先容了hello可执行文件的执行过程,包括进程创建、加载和停止,还探讨了键盘输入对进程产生的影响。从创建进程到回收进程,这一整个过程中需要各种各样的异常和信号。通过对hello进程异常与信号处理的分析,我们对计算机系统的进程管理有了更加深刻的熟悉。

第7章 hello的存储管理

7.1 hello的存储器地址空间

(1)逻辑地址:逻辑地址是程序中使用的地址,由程序员或编译器定义。在程序中,逻辑地址是相对于程序自身的地址空间的,它是程序员编写代码时使用的地址。
(2)线性地址:线性地址是逻辑地址颠末分段机制转换之后得到的地址。在分段机制下,逻辑地址被分为多个段,每个段有一个基地址,线性地址是通过将逻辑地址的偏移量与相应段的基地址相加而得到的。
(3)假造地址:假造地址是在程序执行时由CPU天生的地址,它是逻辑地址或线性地址到物理地址的中间层。假造地址空间是一个抽象的地址范围,程序认为自己独占整个地址空间,而实际上是与其他程序共享的。
(4)物理地址:理地址是最终在RAM中实际存在的地址。操纵系统通过内存管理单元将假造地址映射到物理地址,从而使程序能够访问实际的硬件内存。
7.2 Intel逻辑地址到线性地址的变换-段式管理

Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。内存地址由两个部分组成:段选择符和偏移量。逻辑地址通过这两个部分颠末一系列的变换得到线性地址。以段选择符为索引,在GDT或LDT中找到对应的段描述符。

图 70 段选择符各字段寄义

将段描述符中的基地址与偏移量相加,得到线性地址。假如启用了分页机制,则线性地址可能还需要进一步转换为物理地址,否则线性地址就是物理地址。
7.3 Hello的线性地址到物理地址的变换-页式管理

线性地址到物理地址之间的转换通过对假造地址内存空间进行分页的分页机制完成。假造地址可被分为两个部分:VPN(假造页号)和VPO(假造页偏移量)。

图 71 32位系统线性地址向物理地址变换

线性地址的高位部分颠末页表的查找得到相应的页表项,当页表项有效位为 1 时,操纵系统或硬件使用该页表项进行假造地址到物理地址的映射。页表项中包含物理页框号以及一些控制信息。将页表项中的物理页框号与线性地址的低位部分(偏移量)相加,得到最终的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换


图 72 TLB与四级页表支持下的VA到PA的变换(左半部分)

如上图左半部分所示,CPU产生假造地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若掷中,则得到PPN(40位)与VPO(12位)组合成物理地址PA(52位)。若TLB没有掷中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9位)确定在第一级页表中的偏移量,查询出PTE,假如在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。
7.5 三级Cache支持下的物理内存访问

CPU将一条假造地址VA传送到MMU按照7.4所述的操纵获得了物理地址PA。如上图71右半部分所示,根据cache巨细组数的要求,将PA分为CT(标志位)、CI(组索引)、CO(块偏移)。根据CI寻找到正确的组,依次与每一行的数据比较,有效位有效且标志位一致则掷中。假如掷中,直接返回想要的数据。假如不掷中,就依次去L2、L3、主存判定是否掷中,掷中时将数据传给CPU同时更新各级cache的储存。
7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建假造内存,内核创建了当前进程的mm_struct、地区结构和页表的原样副本。它将两个进程中的每个页面都标志为只读,并将两个进程中的每个地区结构都标志为私有的写时复制。
当fork在新进程中返回时,新进程如今的假造内存刚好和调用fork时存在的假造内存相同。当这两个进程中的任一个厥后进行写操纵时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射

(1)删除已存在的用户地区:删除当前进程假造地址的用户部分中的已存在的地区结构。
(2)映射私有地区:为新程序的代码、数据、bss和栈地区创建新的地区结构。所有这些新的地区都是私有的、写时复制的。代码和数据地区被映射为a.out文件中的.text和.data区。bss地区是哀求二进制零的,映射到匿名文件,其巨细包含在a.out中。栈堆地区也是哀求二进制零的,初始长度为零。
(3)映射共享地区:假如a.out程序与共享对象(或目的)链接,好比标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户假造地址空间中的共享地区内。
(4)设置程序计数器(PC):execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码地区的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

图 73 加载器映射用户地址空间地区的方式

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

概念:
缺页故障是计算机操纵系统中的一种情况,指的是程序试图访问在物理内存中不存在的页面。当出现缺页故障时,操纵系统需要进行相应的缺页中断处理。
缺页中断的处理:
(1)缺页异常的产生:当程序尝试访问一个假造地址,而对应的页面不在物理内存中时,就会发生缺页故障。地址翻译硬件从内存中读取CPU引用信息对应的PTE,从有效位推断出对应的页未被缓存,触发缺页异常。 在进入异常处理程序之前,硬件会主动保存一些寄存器的值,以便在中断处理竣事后能够正确地恢复执行。
(2)缺页异常处理程序:缺页异常调用缺页异常处理程序,该程序会选择一个牺牲页,若此页已被修改,内核会将其复制会磁盘。无论哪种情况,内核都会修改相应页表条目,反映牺牲页不再缓存在主存中。异常处理程序将缺页对应的页面从磁盘加载到主存中,更新页表,随后返回。
(3)正常继承运行:恢复之前保存的寄存器值,以便继承执行用户程序。此时,用户程序能够重新访问之前导致缺页故障的假造地址。由于页面已经加载到物理内存,重新执行引起缺页故障的指令。这次访问将成功完成。
7.9动态存储分配管理

动态内存分配器维护着一个称为堆的进程的假造内存地区。分配器将堆视为一组不同巨细的块的聚集来维护。每个块就是一个一连的假造内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被开释,这种开释可以由应用程序显式执行或内存分配器自身隐式执行。
分配器有两种基本风格,显式分配器和隐式分配器。显示分配器要求应用显式地开释任何已分配的块;隐式分配器要求分配器检测一个已分配块何时不再使用,那么就开释这个块,主动开释未使用的已经分配的块的过程叫做垃圾收集。
7.10本章小结

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

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模子化:文件;

设备管理:unix io接口。

Linux中所有的IO设备都被模子化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种同一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。

8.2 简述Unix IO接口及其函数

一、描述符管理
(1)打开文件:
int open(const char *path, int flags, mode_t mode): 打开文件并返回文件描述符。
(2)关闭文件:
int close(int fd): 关闭文件描述符。
二、读写操纵
(1)读取数据:
ssize_t read(int fd, void *buf, size_t count): 从文件描述符中读取数据。
(2)写入数据:
ssize_t write(int fd, const void *buf, size_t count): 向文件描述符写入数据。
三、文件控制
(1)设置文件指针位置:
off_t lseek(int fd, off_t offset, int whence): 设置文件描述符的读/写位置。
(2)读取文件元数据:
int stat(const char *path, struct stat *buf): 获取文件的具体信息。
8.3 printf的实现分析

printf在Linux内核中的定义如下:

图 74 printf实现

其首先声明一个长度1024的缓冲区,之后创建可变参数表,随后调用vsprintf将可变参数填入格式字符串进行格式化,放到缓冲区中,随后打印缓冲区内信息。
vsprintf的实现如下:

图 75 vfprintf的实现

其重要作用为格式化字符串,它接受确定输特别式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出并返回字符串长度。
之后调用puts函数,实在现方式如下:

图 76 puts等函数的实现

对于每个字符,puts调用putchar,putchar再根据是否处于早期通过串口输出调试信息判定是否向串口输出信息,在bios_putchar中,程序设置寄存器,并将设置好的寄存器值传递给BIOS中断0x10,以执行字符输出操纵。也可以通过write系统函数,系统调用 int 0x80或syscall以实现该过程。
字符显示驱动子程序从字符的ASCII编码到字模库中查询,将字模信息转移到到显示vram中,此时显示VRAM中存储了每一个点的RGB颜色信息。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析

Linux内核中getchar的实现如下:

图 77 getchar的实现

首先实例化两个寄存器结构体,对一个进行初始化,之后传递给BIOS中断0x16。BIOS的中断0x16用于处理键盘输入。这个中断是基本的键盘输入服务例程,允许程序从键盘获取用户输入。

图 78 BIOS中断0x16功能

通常,这个功能返回时,AH 寄存器中包含了按下按键的扫描码,而 AL 寄存器中则包含了按下按键的ASCII码。操纵系统内,getchar等调用read系统函数,通过系统调用读取按键ascii码。
8.5本章小结

本章重要先容了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数,最后通过对printf函数和getchar函数的底层实现的分析,对其工作过程有了基本了解。
结论

hello所履历的过程:
(1)预处理
对hello.c进行宏替换、头文件包含和条件编译等操纵。预处理器将天生一个颠末处理的源代码文件hello.i。
(2)编译
编译阶段包括词法分析、语法分析、语义分析、优化和代码天生等步调。编译器将预处理后的文件转换为汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s。
(3)汇编
汇编阶段将汇编代码翻译成呆板语言,并天生可重定位目的文件hello.o。
(4)链接
通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,天生完全链接的可执行的目的文件hello。
(5)加载运行
打开Shell,在通过IO设备在其中输入./hello 2022112XXX XXX 2,终端fork新建进程,并通过execve把代码和数据加载入假造内存空间,程序开始执行。
(6)执行指令
CPU按照程序计数器(PC)指向的指令地址,从内存中读取指令并执行。程序的控制逻辑流通过CPU的执行来实现。
(7)访存
内存管理单元(MMU)负责将程序使用的逻辑地址映射到物理地址,从而访问实际的内存。
(8)信号处理
信号处理允许程序在运行时对外部变乱作出相应,如Ctrl+C(SIGINT)和Ctrl+Z(SIGTSTP)。
(9)停止并被回收
当程序执行完毕或因错误而停止时,操纵系统会回收程序使用的资源,包括关闭文件、开释内存等。
你对计算机系统的操持与实现的深切感悟:
计算机系统的操持与实现是一个复杂而又深刻的过程,它不仅仅是硬件和软件的联合,更是对计算机科学原理、算法、数据结构、操纵系统、编程语言等多个领域深入明白的体现。抽象是计算机科学的紧张头脑,通过得当的抽象,可以简化问题、进步系统的灵活性,并使得不同条理的组件更容易协同工作。对抽象的明白使得系统操持更加灵活、可扩展。

附件

列出所有的中间产物的文件名,并予以阐明起作用。
文件名
作用
hello.c
源代码
hello.i
预处理后得到的文本文件
hello.s
编译后得到的汇编语言文件
hello.o
汇编后得到的可重定位目的文件
hello_dump.s
反汇编hello.o得到的反汇编文件
hello_o.elf
用readelf读取hello.o得到的ELF格式信息
hello
可执行文件
hello_exec.s
反汇编hello可执行文件得到的反汇编文件
hello.elf
由hello可执行文件天生的.elf文件
表 4 中间效果


参考文献

[1]  兰德尔 E.布莱恩特, 大卫 R.奥哈拉伦.深入明白计算机系统:a programmer's perspective[M].机械工业出版社.2016
[2] Pianistx.printf 函数实现的深入剖析[EB/OL].(2013-09-11)[2023-12-24]. https://www.cnblogs.com/pianist/p/3315801.html
[3] Bootlin.printf.c - arch/x86/boot/printf.c - Linux source code (v6.6.8) - Bootlin[EB/OL].(2023-12-20)[2023-12-24]printf.c - arch/x86/boot/printf.c - Linux source code (v6.6.8) - Bootlin





免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张国伟

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表