HIT-CSAPP大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业            信息安全           

学     号            2022113151           

班     级            2203201           

学       生             杨硕林        

指 导 教 师             史先俊          

计算机科学与技术学院

2024年5月

摘  要

    本文以hello.c文件为方式,回顾了计算机系统的底层知识,并通过此方式,巩固了本学期学习的《深入理解计算机系统》的各个内容,hello.c文件通过在Linux虚拟机中编译并执行,然后对编译后的可执行文件hello进行程序的运行的分解,展示一个程序从开始运行到最终结束的过程。

关键词:计算机系统;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简介

Hello程序的生命周期是从一个源程序,或者说源文件的,即程序员通过编辑器创建并保存的文本文件,文件名是hello.c.

在操作系统中hello.c经过预处理器cpp的预处理,生成hello.i文件、编译器ccl的编译生成hello.s文件、汇编器as的汇编,生成hello.o文件、连接器ld的链接最终成为可执行目标程序hello.

然后在shell里面输入 ./hello 后,系统会将这些字符读入到寄存器中,开始将Hello目标文件中的代码和数据从磁盘复制到内存,调用fork函数创建一个新的子进程,子进程通过execve系统调用加载器.操作系统保存shell的上下文,创建一个新的hello进程的上下文,并将控制权转给新的进程.

利用虚拟内存技术,新的代码段和数据段被初始化为hello目标文件的内容.然后,加载器跳转到_start地址,之后会来到 main 函数的地址.

程序从内存读取指令字节,在执行阶段执行指令指明的操作.在流水线化的系统中,待执行的程序被分解成几个阶段,每个阶段完成指令的一部分.Hello就这样被执行了.

Hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回shell,shell进程等待下一个输入.

1.2 环境与工具

硬件环境: 12th Gen Intel(R) Core(TM) i9-12900H

软件环境:Ubuntu-22.04.4

开发与调试工具:gcc,gdb,readelf,HexEdit,ld

1.3 中间结果

中间文件

作用

Hello.c

源程序

Hello.i

预处理生成的文件

Hello.s

编译生成的文件

Hello.o

汇编生成的文件

Hello.elf

Hello.o的elf格式,展示可重定位的elf文件格式

Hello.asm

Hello.o的反汇编格式

Hello

生成的文件

Hello_exe.elf

Hello的elf格式

Hello_exe.asm

Hello的反汇编格式

编译过程及生成文件展示

1.4 本章小结

本章介绍了hello.c的编译过程,并且通过反编译获取了hello.o和hello的反汇编文件,展示了hello的P2P和020的过程:预处理、编译、汇编、链接等。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor)

对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。通常由以下几个操作组成:

头文件展开:将#include包含的文件插入到该指令位置

宏展开:展开所有的宏定义,并删除#define

条件编译:处理所有的条件预编译指令: #if、 #ifdef、 #else

删除注释

添加行号和文件名标识:编译调试时显示行号信息

保留#pragma命令

作用:

预处理器(cpp)把程序中声明的文件复制到这个程序中,具体到hello.c就是#include <unistd.h>

#include <stdlib.h>

#include<stdio.h>

cpp把这些头文件的内容插入到程序文本中,方便编译器进行下一步的编译。

结果就得到了另一个C程序,通常是以.i作为文件扩展名

2.2在Ubuntu下预处理的命令

gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i

在第一章时已对hello.c进行过预处理并得到hello.i文件

2.3 Hello的预处理结果解析

打开hello.i文件:

可以看到有非常多的头文件路径展开,证实了预处理器中的一个作用就是解析头文件的应用并确定具体代码的位置。

接下来的内容出现了很多的“typedef”:

可以看到这里将我们在C语言代码中常见的数据类型定义为了看上去更“机器语言”的别名,便于后面的编译。

在文件的最后可以看到hello.c的源程序:

而此时代码已经到达了三千多行的长度。

这里为了检查下预处理文件的长度是否与源程序无关,重新写了一个新的程序test.c:

预处理后文件:

仍然有三千多行代码,显然预处理文件长度与源程序的函数关系不大。

如果减少头文件数量呢?

修改test.c内容(另存到rtest.c中):

预处理:

可以看到这时候预处理文件只有744行代码 ,因此可以得知预处理大部分的内容均为对引用头文件的处理,也证实了上述对预处理的描述。

2.4 本章小结

本章深入研究了预处理的作用和表现,并通过测试文件作为参照证实了预处理主要为对于头文件和宏定义的展开。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序.该程序包含函数main的定义,这个过程称为编译

编译的作用:把用高级程序设计语言书写的源程序,翻译成等价的计算机汇编语言或机器语言书写的目标程序,并且进行语法检查、调试措施、代码优化、覆盖处理等步骤

3.2 在Ubuntu下编译的命令

指令:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

详见第一章的编译过程

然后得到过程文件hello.s

3.3 Hello的编译结果解析

      1. 全局变量

没有全局变量,略过

      1. main函数的参数

由寄存器相关的知识,我们知道存储函数参数的前六个寄存器分别是%rdi%rsi%rdx%rcs%r8%r9。根据汇编代码,我们可以发现argcargv分别存储在%edi%rsi中,并在一开始首先分别保存到了-20(%rbp)-32(%rbp)的位置。

      1. 局部变量

局部变量一般直接存储在栈中。本题的循环变量i是一个局部变量,观察发现它若能执行到循环分支,则它被初始化为0,且存储在栈中。

      1. 字符串常量

字符串常量一般存储在.text节中。我们在汇编代码中可以发现,我们用到的两个字符串常量均在.text节中存储,并且各自有一个标号。

在使用字符串常量的时候,是直接使用标号来引用的

      1. 比较操作

变量之间的不等关系是通过!=符号来实现的。而在汇编代码中的体现则是用cmp语句。

      1. 分支语句

分支语句是基于3.3.5中的比较语句加jxx跳转语句实现的,通过cmp设置的标志,达到一定条件则执行对应的分支

      1. 循环语句

循环也是通过比较加跳转语句来实现的。初始值是0,和9作比较,总循环次数10

      1. 数组操作

我们知道数组的操作一般都是通过首地址加上偏移量得到的,我们在汇编代码中可以观察到这种方式用在了取argv中的字符串的地址。

argv数组中的内容存储在了栈中,我们从中取出对应的字符串的地址,并分别放到%rdx和%rcx中,作为printf的第二和第三个参数,最终输出到了屏幕上。

      1. 函数调用

函数调用在汇编中的实现很简单,就是调用call指令。在hello.s中执行多次。而函数参数的传递则类似于3.3.2中的描述。前六个参数分别存储于%rdi,%rsi,%rdx,%rcx,%r8,%r9,而剩余的参数则存放在栈中,位于返回地址的上面。

调用puts:

调用printf

调用sleep

      1. 函数返回

一般的函数返回前会有这样几个操作,恢复被调用者保存的寄存器的值,恢复旧的帧指针%rbp(不一定有这个操作),并跳转到原来的控制流的地址。最终一般都是以ret指令结尾的。

3.4 本章小结

本章节主要叙述了编译器是如何处理c语言程序的,结合c语言中的各种数据类型,各类运算,各类函数操作,逐个分析编译器的具体行为

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中

汇编的作用:翻译生成机器语言,因为机器语言是计算机能直接识别和执行的一种语言

4.2 在Ubuntu下汇编的命令

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

截图及过程详见第一章

4.3 可重定位目标elf格式

通过指令:readelf -a -d hello.o -o hello.elf

得到hello.o文件的elf格式,过程及截图见第一章。(第一章的readelf语句出现了错误,这里已经进行了修改并改正了elf文件,下面对hello可执行文件的处理也同)

      1. ELF头:

      1. 节头表:

      1. 重定位节:

4.4 Hello.o的结果解析

这里通过:objdump -d -r hello.o > hello.asm 得到反汇编的结果。

对比分析:

      1. hello.s中没有位置信息,但hello.asm中有,且代码间有顺序关系。

hello.asm:

hello.s:

      1. hello.asm中使用地址进行跳转,而hello.s中使用标号进行跳转

hello.asm:

hello.s:

      1. hello.asm中使用地址进行函数调用,而hello.s中使用函数名进行跳转

hello.asm:

hello.s:

      1. hello.asm中有重定位条目,而hello.s中没有

重定位条目:

4.5 本章小结

本章经历了hellohello.shello.o的汇编过程,查看了hello.oelf,使用objdump工具得到反汇编代码,和之前的汇编代码进行比较,通过寻找不同之处分析了从汇编语言到机器语言的映射关系

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

链接后生成的可执行文件:

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

通过readelf -a -d hello -o hello_exe.elf将文件存储:

elf头:

可以看到在类型一栏变为了可执行文件。

节头:

5.4 hello的虚拟地址空间

使用edb加载hello程序:

通过data dump查看虚拟地址:

5.5 链接的重定位过程分析

通过 objdump -d -r hello > hello_exe.asm 将分析文件存储下来:

重定位的过程如下:

1.在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2, _start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main,连接器将这些函数加入进程序

2.将所有的R_X86_64_PC32和R_X86_64_PLT32替换成计算好的地址,书上给出了R_X86_64_PC32的重定位算法

hello.o与hello的区别:

hello.o的地址采用相对位移量,而hello采用虚拟地址空间的地址:

hello.asm:

hello_exe.asm:

然后是hello中多出了调用函数的定义:

5.6 hello的执行流程

执行edb调试hello程序:

程序地址

程序名

0x00000000004010f0

Hello!_art

0x0000000000401125

Hello!main

0x0000000000401000

Hello!_int

0x00000000004011c0

Hello!_fini

0x0000000000404050

Hello!_end

5.7 Hello的动态链接分析

动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。

反汇编获取gotplt位置:

链接前的got:

链接后的got:

5.8 本章小结

在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程

6章 hello进程管理

6.1 进程的概念与作用

概念:进程就是一个执行中程序的实例,每一个进程都有它自己的地址空间,包括代码段、数据段、和堆栈区。

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

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

Shell的作用: 在linux系统中,Shell 是指一种应用程序,提供了一个界面,用户通过这个界面访问操作系统内核的服务

处理流程:Shell从终端读入输入命令,如果是内置命令则立即执行, 如果不是内部命令并且在列表里没有找到这个可执行文件,将会显示一条错误信息

6.3 Hello的fork进程创建过程

在shell中运行hello时输入./hello 2022113151 杨硕林 18771538208 3

 ./hello是执行当前目录下可执行文件hello的命令,此时终端调用folk函数在当前进程中创建一个与父进程几乎一模一样的子进程,之后内核就可以以任意方式交替执行他们的逻辑控制流中的指令。这样一来,在hello加载到内存中之后,就可以开始执行了。

6.4 Hello的execve过程

execve 函数加载并运行可执行目标文件hello, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到hello, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。

execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段.新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容.最后加载器设置PC指向_start地址,_start最终调用main函数.除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制.直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存.

6.5 Hello的进程执行

上下文信息:上下文程序正确运行所需要的状态,包括存放在内存中的程序的代码和数据,用户栈、用寄存器、程序计数器、环境变量和打开的文件描述符的集合构成

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,这个模式中,硬件防止特权指令的执行,并对内存和I/O空间的访问操作进行检查.设置模式位时,进程处于内核模式,一切程序都可运行.任务可以执行特权级指令,对任何I/O设备有全部的访问权,还能够访问任何虚地址和控制虚拟内存硬件

进程执行分析:Hello起初在用户模式下运行,在hello进程调用sleep之后转入内核模式,内核休眠,并将hello进程从运行队列加入等待队列,定时器开始计时2s,当定时器到时,发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列, hello进程继续执行

6.6 hello的异常与信号处理

Hello出现的异常:中断、陷阱(系统调用)

Hello产生的信号:SIGINT、SIGINTSTP、SIGCONT、SIGCHLD

1. 输入Ctrl+c:

进程收到了SIGINT信号

2. 输入Ctrl+z:

进程收到了SIGSTP信号

3. 用jobs观察:

4. 用pstree看进程树:

通过kill传入SIGKILL信号杀死进程:

输入SIGSCONT信号:

6.7本章小结

在本章中,介绍了进程的定义和作用,介绍了Shell的处理流程,介绍了shell如何调用fork创建新进程,如何调用execve执行hello,分析了hello的进程执行,还研究了hello的异常与信号处理

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:由程序产生的与段相关的偏移地址部分。一个逻辑地址由两部份组成,段标识符和段内偏移量。页式存储器的逻辑地址由两部分组成:页号和页内地址。[段标识符 : 段内偏移地址] 的表示形式,其中的段内偏移地址就是指逻辑地址;当我们调用printf函数时,编译器会生成一个调用printf函数的指令,这个指令中的地址就是一个逻辑地址。

线性地址:是逻辑地址到物理地址变换之间的中间层。hello的代码产生的段中的偏移地址,加上相应段的基地址构成一个线性地址。Hello.o反汇编中每个函数可见这种表示方式。

虚拟地址是线性地址经过分页机制转换后得到的地址。操作系统使用页表将线性地址映射到虚拟地址。虚拟地址空间允许每个进程有自己独立的地址空间,从而提高了安全性和稳定性。当hello程序运行时,操作系统为其分配一个虚拟地址空间。Hello反汇编中可以看到都有固定的地址。

物理地址:物理地址是最终的内存地址,即实际的硬件内存地址。虚拟地址通过页表映射到物理地址,CPU通过内存管理单元(MMU)完成这个转换。例如,当hello程序调用printf函数时,虚拟地址通过页表转换为物理地址,CPU最终访问这个物理地址来执行函数。

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

每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT).而要想找到某个段的描述符必须通过段选择符才能找到.

段选择符由三个部分组成,从右向左依次是RPL、TI、index(索引).先来看TI,当TI=0时,表示段描述符在GDT中,当TI=1时表示段描述符在LDT中.

再来看一下index部分.我们可以将描述符表看成是一个数组,每个元素都存放一个段描述符,那index就表示某个段描述符在数组中的索引.

假设GDT的起始位置是0x00020000,而一个段描述符的大小是8个字节,由此我们可以计算出段描述符所在的地址:0x00020000+8*index,从而我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址

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

分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。

为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。32位的线性地址被分成3个部分:最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。

每个活动的任务,必须要先分配给它一个页目录表,并把页目录表的物理地址存入cr3寄存器。页表可以提前分配好,也可以在用到的时候再分配。

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

TLB(Translation Lookaside Buffer)翻译后备缓冲器,用于加速地址的翻译

使用TLB时翻译地址的一般处理流程:

CPU 产生一个虚拟地址。

MMU TLB 中取出相应的 PTE

MMU 将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。

高速缓存/主存将所请求的数据字返回给 CPU

多级页表:将原来的一级页表拆分为多级,每一级保存着下一级页表的索引,减小内存的压力和资源的浪费

四级页表会将虚拟地址的VPN部分分为4部分,每部分对应一级页表,MMU会依次查询每一级页表,最后查询得到物理地址的偏移量PPN,和虚拟地址的低p位拼接得到完整的物理地址

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

在现代计算机系统中,为了提高内存访问的速度,通常会使用多级缓存(Cache)。

7.5.1 缓存层级结构

(1)一级缓存(L1 Cache)

位置:最接近CPU核心,通常分为两个部分:指令缓存(L1i)和数据缓存(L1d)。

大小:通常较小(几KB到几十KB)。

速度:非常快,延迟通常在1到3个时钟周期。

(2)二级缓存(L2 Cache)

位置:紧接L1缓存,可能是每个CPU核心独有,也可能是每两个核心共享。

大小:比L1大(几百KB到几MB)。

速度:稍慢于L1缓存,延迟通常在10到20个时钟周期。

(3)三级缓存(L3 Cache)

位置:通常为整个处理器共享,所有核心都可以访问。

大小:较大(几MB到几十MB)。

速度:慢于L2缓存,延迟通常在几十到上百个时钟周期。

7.5.2 缓存访问过程

当CPU需要访问某个物理地址时,三级缓存架构的访问过程如下:

(1)CPU发出内存访问请求

CPU生成一个物理地址来访问数据(假设地址为PA)。

(2)L1缓存查找

CPU首先在L1缓存中查找PA。如果命中(hit),L1缓存返回数据给CPU,访问结束。如果未命中(miss),请求发送到L2缓存。

(3)L2缓存查找

在L2缓存中查找PA。如果命中(hit),L2缓存返回数据给CPU,并且可能将数据复制到L1缓存。如果未命中(miss),请求发送到L3缓存。

(3)L3缓存查找

在L3缓存中查找PA。如果命中(hit),L3缓存返回数据给CPU,并且可能将数据复制到L2和L1缓存。如果未命中(miss),请求发送到主内存(DRAM)。

(4)内存访问

在L3缓存未命中的情况下,访问请求发送到主内存。主内存返回数据给L3缓存,并且可能复制到L2和L1缓存。最终,数据从L1缓存返回给CPU。

7.5.3 缓存一致性

为了确保多核处理器中所有核心对内存的一致视图,通常采用缓存一致性协议(如MESI、MOESI)。这些协议管理缓存之间的数据一致性,确保当一个核心修改缓存中的数据时,其他核心能够看到最新的数据。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

删除已存在的用户区域

创建新的区域结构: 代码和初始化数据映射到.text和.data区(目标文件提供), .bss和栈映射到匿名文件

设置PC,指向代码区域的入口点

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

当出现缺页故障时,即DRAM缓存不命中,此时调用缺页处理程序,内存会确定一个牺牲页,若页面被修改,则换出到磁盘,再将新的目标页替换牺牲页写入,缺页处理程序页面调入新的页面,并更新内存中的 PTE。缺页处理程序返回到原来的进程,重启导致缺页的指令。

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的存储器地址空间、intel的段式管理、页式管理,TLB与四级页表支持下的VAPA的变换、三级cache支持下物理内存访问, hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:将设备抽象成文件

设备管理:通过unix io接口管理

8.2 简述Unix IO接口及其函数

1. Unix/IO接口:

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

STDIN_FILENO、

STDOUT_FILENO 、

STDERR_FILENO,

它们可用来代替显式的描述符值。

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

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

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

2. Unix/IO函数:

(1). open:打开文件

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open(char *filename, int flags, mode_t mode);

// 返回:若成功则为新文件描述符,若出错为 -1。

flags 参数指明了进程打算如何访问这个文件:

O_RDONLY:只读。

O_WRONLY:只写。

O_RDWR:可读可写。

mode 参数指定了新文件的访问权限位

(2). close:关闭文件

#include <unistd.h>

int close(int fd);

// 返回:若成功则为 0,若出错则为 -1

(3). 读写文件

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t n);

// 返回:若成功则为读的字节数,若 EOF 则为0,若出错为 -1。

ssize_t write(int fd, const void *buf, size_t n);

// 返回:若成功则为写的字节数,若出错则为 -1。

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

write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置。图 10-3 展示了一个程序使用 read 和 write 调用一次一个字节地从标准输入复制到标准输出。

8.3 printf的实现分析

printf函数为va_list arg = (va_list)((char*)(&fmt) + 4);

typedef char *va_list是一个字符指针。(char*)(&fmt) + 4) 表示的是...中的第一个参数。因为 fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。

(1)从vsprintf生成显示信息,

i = vsprintf(buf, fmt, arg); vsprintf返回的是要打印出来的字符串的长度。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

(2)write系统函数

write(buf, i);write函数的功能就是执行一个写操作。以我们学过的知识可知,写操作是计算机的底层操作,是对计算机硬件进行的操作。通过中断门,来实现特定的系统服务。

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

陷阱-系统调用

init_idt_desc(INT_VECTOR_SYS_CALL,DA_386IGate,sys_call,PRIVILEGE_USER);调用sys_call显示格式化了的字符串。

字符显示驱动子程序

从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

(6)显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

要分析他的实现与作用,我们需要首先来看看geychar()的函数体

Getchar()函数的作用是从stdin流中读入一个字符

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求。键盘中断服务程序先从键盘接口取得按键的扫描码,然后根据其扫描码判断用户所按的键并作相应的处理,最后通知中断控制器本次中断结束并实现中断返回。

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

8.5本章小结

输入输出看似简单,实际是一个非常精巧的过程,从程序发出请求到系统函数调用到设备相应,需要执行许多步骤,往往也是拖慢程序的主要因素和一些崩溃异常的高发地,需要谨慎选用函数、命令实现目的。

结论

到这里Hello的整个生命进程就结束了,借助操作系统的帮助,连一点痕迹都没有留下。下面我们来分析一下Hello.c是如何一步一步变成可执行的程序,并最终让我们在命令行看到他的:

  1. 首先,通过文本编辑器等,将hello.c的内容写入文件
  2. 然后经过gcc这个强大的编译器
    1. 预处理,生成完成的c程序文件hello.i
    2. 编译,生成汇编语言描述的文件hello.s
    3. 汇编,将汇编语言文件转化为可重定位目标文件hello.o
    4. 链接,添加必要的模块和函数库,连接成可以直接复制到内存中执行的可执行文件hello
  3. 命令行中输入./hello调用hello文件时,shell会先调用fork创建子进程,并在其中由 execve.创建虚拟内存空间映射、将虚拟内存地址翻译为物理地址、调用高速缓存与缺页处理将hello加载进内存与cpu
  4. cpu取到指令后,控制 hello的逻辑流进行运行,其间调用printfgetchar等函数调用IO设备,进行屏幕的显示和键盘读入
  5. 异常处理:对于运行程序时键盘输入的ctrl-cctrl-z等指令系统中断并调用相应的信号处理程序进行处理

回顾hello的完整执行流程,我了解了现代计算机的各种先进的硬件和精妙的设计思想,知道了在程序设计之外还有这么广阔的计算机系统的世界,加深了对计算机学习的兴趣。

附件

  1. Hello.c:源程序
  2. Hello.i:预处理生成文件
  3. Hello.s:编译生成文件
  4. Hello.o:汇编生成文件
  5. Hello.elf:hello.o的elf格式
  6. Hello.asm:hello.o的反汇编文件
  7. Hello:链接生成的可执行文件
  8. Hello_exe.elf:hello的elf格式
  9. Hello_exe.asm:hello的反汇编文件
  10. Test.c:在预处理阶段进行对头文件展开的验证
  11. Test.i:test.c的预处理文件
  12. Rtest.c:作为对照组参考
  13. Rtest.i:rtest.c的预处理文件

参考文献

[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]

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业            信息安全           

学     号            2022113151           

班     级            2203201           

学       生             杨硕林        

指 导 教 师             史先俊          

计算机科学与技术学院

2024年5月

摘  要

    本文以hello.c文件为方式,回顾了计算机系统的底层知识,并通过此方式,巩固了本学期学习的《深入理解计算机系统》的各个内容,hello.c文件通过在Linux虚拟机中编译并执行,然后对编译后的可执行文件hello进行程序的运行的分解,展示一个程序从开始运行到最终结束的过程。

关键词:计算机系统;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简介

Hello程序的生命周期是从一个源程序,或者说源文件的,即程序员通过编辑器创建并保存的文本文件,文件名是hello.c.

在操作系统中hello.c经过预处理器cpp的预处理,生成hello.i文件、编译器ccl的编译生成hello.s文件、汇编器as的汇编,生成hello.o文件、连接器ld的链接最终成为可执行目标程序hello.

然后在shell里面输入 ./hello 后,系统会将这些字符读入到寄存器中,开始将Hello目标文件中的代码和数据从磁盘复制到内存,调用fork函数创建一个新的子进程,子进程通过execve系统调用加载器.操作系统保存shell的上下文,创建一个新的hello进程的上下文,并将控制权转给新的进程.

利用虚拟内存技术,新的代码段和数据段被初始化为hello目标文件的内容.然后,加载器跳转到_start地址,之后会来到 main 函数的地址.

程序从内存读取指令字节,在执行阶段执行指令指明的操作.在流水线化的系统中,待执行的程序被分解成几个阶段,每个阶段完成指令的一部分.Hello就这样被执行了.

Hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传回shell,shell进程等待下一个输入.

1.2 环境与工具

硬件环境: 12th Gen Intel(R) Core(TM) i9-12900H

软件环境:Ubuntu-22.04.4

开发与调试工具:gcc,gdb,readelf,HexEdit,ld

1.3 中间结果

中间文件

作用

Hello.c

源程序

Hello.i

预处理生成的文件

Hello.s

编译生成的文件

Hello.o

汇编生成的文件

Hello.elf

Hello.o的elf格式,展示可重定位的elf文件格式

Hello.asm

Hello.o的反汇编格式

Hello

生成的文件

Hello_exe.elf

Hello的elf格式

Hello_exe.asm

Hello的反汇编格式

编译过程及生成文件展示

1.4 本章小结

本章介绍了hello.c的编译过程,并且通过反编译获取了hello.o和hello的反汇编文件,展示了hello的P2P和020的过程:预处理、编译、汇编、链接等。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor)

对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。通常由以下几个操作组成:

头文件展开:将#include包含的文件插入到该指令位置

宏展开:展开所有的宏定义,并删除#define

条件编译:处理所有的条件预编译指令: #if、 #ifdef、 #else

删除注释

添加行号和文件名标识:编译调试时显示行号信息

保留#pragma命令

作用:

预处理器(cpp)把程序中声明的文件复制到这个程序中,具体到hello.c就是#include <unistd.h>

#include <stdlib.h>

#include<stdio.h>

cpp把这些头文件的内容插入到程序文本中,方便编译器进行下一步的编译。

结果就得到了另一个C程序,通常是以.i作为文件扩展名

2.2在Ubuntu下预处理的命令

gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i

在第一章时已对hello.c进行过预处理并得到hello.i文件

2.3 Hello的预处理结果解析

打开hello.i文件:

可以看到有非常多的头文件路径展开,证实了预处理器中的一个作用就是解析头文件的应用并确定具体代码的位置。

接下来的内容出现了很多的“typedef”:

可以看到这里将我们在C语言代码中常见的数据类型定义为了看上去更“机器语言”的别名,便于后面的编译。

在文件的最后可以看到hello.c的源程序:

而此时代码已经到达了三千多行的长度。

这里为了检查下预处理文件的长度是否与源程序无关,重新写了一个新的程序test.c:

预处理后文件:

仍然有三千多行代码,显然预处理文件长度与源程序的函数关系不大。

如果减少头文件数量呢?

修改test.c内容(另存到rtest.c中):

预处理:

可以看到这时候预处理文件只有744行代码 ,因此可以得知预处理大部分的内容均为对引用头文件的处理,也证实了上述对预处理的描述。

2.4 本章小结

本章深入研究了预处理的作用和表现,并通过测试文件作为参照证实了预处理主要为对于头文件和宏定义的展开。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序.该程序包含函数main的定义,这个过程称为编译

编译的作用:把用高级程序设计语言书写的源程序,翻译成等价的计算机汇编语言或机器语言书写的目标程序,并且进行语法检查、调试措施、代码优化、覆盖处理等步骤

3.2 在Ubuntu下编译的命令

指令:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

详见第一章的编译过程

然后得到过程文件hello.s

3.3 Hello的编译结果解析

      1. 全局变量

没有全局变量,略过

      1. main函数的参数

由寄存器相关的知识,我们知道存储函数参数的前六个寄存器分别是%rdi%rsi%rdx%rcs%r8%r9。根据汇编代码,我们可以发现argcargv分别存储在%edi%rsi中,并在一开始首先分别保存到了-20(%rbp)-32(%rbp)的位置。

      1. 局部变量

局部变量一般直接存储在栈中。本题的循环变量i是一个局部变量,观察发现它若能执行到循环分支,则它被初始化为0,且存储在栈中。

      1. 字符串常量

字符串常量一般存储在.text节中。我们在汇编代码中可以发现,我们用到的两个字符串常量均在.text节中存储,并且各自有一个标号。

在使用字符串常量的时候,是直接使用标号来引用的

      1. 比较操作

变量之间的不等关系是通过!=符号来实现的。而在汇编代码中的体现则是用cmp语句。

      1. 分支语句

分支语句是基于3.3.5中的比较语句加jxx跳转语句实现的,通过cmp设置的标志,达到一定条件则执行对应的分支

      1. 循环语句

循环也是通过比较加跳转语句来实现的。初始值是0,和9作比较,总循环次数10

      1. 数组操作

我们知道数组的操作一般都是通过首地址加上偏移量得到的,我们在汇编代码中可以观察到这种方式用在了取argv中的字符串的地址。

argv数组中的内容存储在了栈中,我们从中取出对应的字符串的地址,并分别放到%rdx和%rcx中,作为printf的第二和第三个参数,最终输出到了屏幕上。

      1. 函数调用

函数调用在汇编中的实现很简单,就是调用call指令。在hello.s中执行多次。而函数参数的传递则类似于3.3.2中的描述。前六个参数分别存储于%rdi,%rsi,%rdx,%rcx,%r8,%r9,而剩余的参数则存放在栈中,位于返回地址的上面。

调用puts:

调用printf

调用sleep

      1. 函数返回

一般的函数返回前会有这样几个操作,恢复被调用者保存的寄存器的值,恢复旧的帧指针%rbp(不一定有这个操作),并跳转到原来的控制流的地址。最终一般都是以ret指令结尾的。

3.4 本章小结

本章节主要叙述了编译器是如何处理c语言程序的,结合c语言中的各种数据类型,各类运算,各类函数操作,逐个分析编译器的具体行为

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中

汇编的作用:翻译生成机器语言,因为机器语言是计算机能直接识别和执行的一种语言

4.2 在Ubuntu下汇编的命令

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

截图及过程详见第一章

4.3 可重定位目标elf格式

通过指令:readelf -a -d hello.o -o hello.elf

得到hello.o文件的elf格式,过程及截图见第一章。(第一章的readelf语句出现了错误,这里已经进行了修改并改正了elf文件,下面对hello可执行文件的处理也同)

      1. ELF头:

      1. 节头表:

      1. 重定位节:

4.4 Hello.o的结果解析

这里通过:objdump -d -r hello.o > hello.asm 得到反汇编的结果。

对比分析:

      1. hello.s中没有位置信息,但hello.asm中有,且代码间有顺序关系。

hello.asm:

hello.s:

      1. hello.asm中使用地址进行跳转,而hello.s中使用标号进行跳转

hello.asm:

hello.s:

      1. hello.asm中使用地址进行函数调用,而hello.s中使用函数名进行跳转

hello.asm:

hello.s:

      1. hello.asm中有重定位条目,而hello.s中没有

重定位条目:

4.5 本章小结

本章经历了hellohello.shello.o的汇编过程,查看了hello.oelf,使用objdump工具得到反汇编代码,和之前的汇编代码进行比较,通过寻找不同之处分析了从汇编语言到机器语言的映射关系

第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

链接后生成的可执行文件:

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

通过readelf -a -d hello -o hello_exe.elf将文件存储:

elf头:

可以看到在类型一栏变为了可执行文件。

节头:

5.4 hello的虚拟地址空间

使用edb加载hello程序:

通过data dump查看虚拟地址:

5.5 链接的重定位过程分析

通过 objdump -d -r hello > hello_exe.asm 将分析文件存储下来:

重定位的过程如下:

1.在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2, _start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main,连接器将这些函数加入进程序

2.将所有的R_X86_64_PC32和R_X86_64_PLT32替换成计算好的地址,书上给出了R_X86_64_PC32的重定位算法

hello.o与hello的区别:

hello.o的地址采用相对位移量,而hello采用虚拟地址空间的地址:

hello.asm:

hello_exe.asm:

然后是hello中多出了调用函数的定义:

5.6 hello的执行流程

执行edb调试hello程序:

程序地址

程序名

0x00000000004010f0

Hello!_art

0x0000000000401125

Hello!main

0x0000000000401000

Hello!_int

0x00000000004011c0

Hello!_fini

0x0000000000404050

Hello!_end

5.7 Hello的动态链接分析

动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。

反汇编获取gotplt位置:

链接前的got:

链接后的got:

5.8 本章小结

在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程就是一个执行中程序的实例,每一个进程都有它自己的地址空间,包括代码段、数据段、和堆栈区。

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

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

Shell的作用: 在linux系统中,Shell 是指一种应用程序,提供了一个界面,用户通过这个界面访问操作系统内核的服务

处理流程:Shell从终端读入输入命令,如果是内置命令则立即执行, 如果不是内部命令并且在列表里没有找到这个可执行文件,将会显示一条错误信息

6.3 Hello的fork进程创建过程

在shell中运行hello时输入./hello 2022113151 杨硕林 18771538208 3

 ./hello是执行当前目录下可执行文件hello的命令,此时终端调用folk函数在当前进程中创建一个与父进程几乎一模一样的子进程,之后内核就可以以任意方式交替执行他们的逻辑控制流中的指令。这样一来,在hello加载到内存中之后,就可以开始执行了。

6.4 Hello的execve过程

execve 函数加载并运行可执行目标文件hello, 且带参数列表argv 和环境变量列表envp 。只有当出现错误时,例如找不到hello, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。

execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段.新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容.最后加载器设置PC指向_start地址,_start最终调用main函数.除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制.直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存.

6.5 Hello的进程执行

上下文信息:上下文程序正确运行所需要的状态,包括存放在内存中的程序的代码和数据,用户栈、用寄存器、程序计数器、环境变量和打开的文件描述符的集合构成

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,这个模式中,硬件防止特权指令的执行,并对内存和I/O空间的访问操作进行检查.设置模式位时,进程处于内核模式,一切程序都可运行.任务可以执行特权级指令,对任何I/O设备有全部的访问权,还能够访问任何虚地址和控制虚拟内存硬件

进程执行分析:Hello起初在用户模式下运行,在hello进程调用sleep之后转入内核模式,内核休眠,并将hello进程从运行队列加入等待队列,定时器开始计时2s,当定时器到时,发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列, hello进程继续执行

6.6 hello的异常与信号处理

Hello出现的异常:中断、陷阱(系统调用)

Hello产生的信号:SIGINT、SIGINTSTP、SIGCONT、SIGCHLD

1. 输入Ctrl+c:

进程收到了SIGINT信号

2. 输入Ctrl+z:

进程收到了SIGSTP信号

3. 用jobs观察:

4. 用pstree看进程树:

通过kill传入SIGKILL信号杀死进程:

输入SIGSCONT信号:

6.7本章小结

在本章中,介绍了进程的定义和作用,介绍了Shell的处理流程,介绍了shell如何调用fork创建新进程,如何调用execve执行hello,分析了hello的进程执行,还研究了hello的异常与信号处理

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:由程序产生的与段相关的偏移地址部分。一个逻辑地址由两部份组成,段标识符和段内偏移量。页式存储器的逻辑地址由两部分组成:页号和页内地址。[段标识符 : 段内偏移地址] 的表示形式,其中的段内偏移地址就是指逻辑地址;当我们调用printf函数时,编译器会生成一个调用printf函数的指令,这个指令中的地址就是一个逻辑地址。

线性地址:是逻辑地址到物理地址变换之间的中间层。hello的代码产生的段中的偏移地址,加上相应段的基地址构成一个线性地址。Hello.o反汇编中每个函数可见这种表示方式。

虚拟地址是线性地址经过分页机制转换后得到的地址。操作系统使用页表将线性地址映射到虚拟地址。虚拟地址空间允许每个进程有自己独立的地址空间,从而提高了安全性和稳定性。当hello程序运行时,操作系统为其分配一个虚拟地址空间。Hello反汇编中可以看到都有固定的地址。

物理地址:物理地址是最终的内存地址,即实际的硬件内存地址。虚拟地址通过页表映射到物理地址,CPU通过内存管理单元(MMU)完成这个转换。例如,当hello程序调用printf函数时,虚拟地址通过页表转换为物理地址,CPU最终访问这个物理地址来执行函数。

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

每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT).而要想找到某个段的描述符必须通过段选择符才能找到.

段选择符由三个部分组成,从右向左依次是RPL、TI、index(索引).先来看TI,当TI=0时,表示段描述符在GDT中,当TI=1时表示段描述符在LDT中.

再来看一下index部分.我们可以将描述符表看成是一个数组,每个元素都存放一个段描述符,那index就表示某个段描述符在数组中的索引.

假设GDT的起始位置是0x00020000,而一个段描述符的大小是8个字节,由此我们可以计算出段描述符所在的地址:0x00020000+8*index,从而我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址

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

分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。

为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。32位的线性地址被分成3个部分:最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。

每个活动的任务,必须要先分配给它一个页目录表,并把页目录表的物理地址存入cr3寄存器。页表可以提前分配好,也可以在用到的时候再分配。

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

TLB(Translation Lookaside Buffer)翻译后备缓冲器,用于加速地址的翻译

使用TLB时翻译地址的一般处理流程:

CPU 产生一个虚拟地址。

MMU TLB 中取出相应的 PTE

MMU 将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。

高速缓存/主存将所请求的数据字返回给 CPU

多级页表:将原来的一级页表拆分为多级,每一级保存着下一级页表的索引,减小内存的压力和资源的浪费

四级页表会将虚拟地址的VPN部分分为4部分,每部分对应一级页表,MMU会依次查询每一级页表,最后查询得到物理地址的偏移量PPN,和虚拟地址的低p位拼接得到完整的物理地址

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

在现代计算机系统中,为了提高内存访问的速度,通常会使用多级缓存(Cache)。

7.5.1 缓存层级结构

(1)一级缓存(L1 Cache)

位置:最接近CPU核心,通常分为两个部分:指令缓存(L1i)和数据缓存(L1d)。

大小:通常较小(几KB到几十KB)。

速度:非常快,延迟通常在1到3个时钟周期。

(2)二级缓存(L2 Cache)

位置:紧接L1缓存,可能是每个CPU核心独有,也可能是每两个核心共享。

大小:比L1大(几百KB到几MB)。

速度:稍慢于L1缓存,延迟通常在10到20个时钟周期。

(3)三级缓存(L3 Cache)

位置:通常为整个处理器共享,所有核心都可以访问。

大小:较大(几MB到几十MB)。

速度:慢于L2缓存,延迟通常在几十到上百个时钟周期。

7.5.2 缓存访问过程

当CPU需要访问某个物理地址时,三级缓存架构的访问过程如下:

(1)CPU发出内存访问请求

CPU生成一个物理地址来访问数据(假设地址为PA)。

(2)L1缓存查找

CPU首先在L1缓存中查找PA。如果命中(hit),L1缓存返回数据给CPU,访问结束。如果未命中(miss),请求发送到L2缓存。

(3)L2缓存查找

在L2缓存中查找PA。如果命中(hit),L2缓存返回数据给CPU,并且可能将数据复制到L1缓存。如果未命中(miss),请求发送到L3缓存。

(3)L3缓存查找

在L3缓存中查找PA。如果命中(hit),L3缓存返回数据给CPU,并且可能将数据复制到L2和L1缓存。如果未命中(miss),请求发送到主内存(DRAM)。

(4)内存访问

在L3缓存未命中的情况下,访问请求发送到主内存。主内存返回数据给L3缓存,并且可能复制到L2和L1缓存。最终,数据从L1缓存返回给CPU。

7.5.3 缓存一致性

为了确保多核处理器中所有核心对内存的一致视图,通常采用缓存一致性协议(如MESI、MOESI)。这些协议管理缓存之间的数据一致性,确保当一个核心修改缓存中的数据时,其他核心能够看到最新的数据。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

删除已存在的用户区域

创建新的区域结构: 代码和初始化数据映射到.text和.data区(目标文件提供), .bss和栈映射到匿名文件

设置PC,指向代码区域的入口点

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

当出现缺页故障时,即DRAM缓存不命中,此时调用缺页处理程序,内存会确定一个牺牲页,若页面被修改,则换出到磁盘,再将新的目标页替换牺牲页写入,缺页处理程序页面调入新的页面,并更新内存中的 PTE。缺页处理程序返回到原来的进程,重启导致缺页的指令。

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的存储器地址空间、intel的段式管理、页式管理,TLB与四级页表支持下的VAPA的变换、三级cache支持下物理内存访问, hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:将设备抽象成文件

设备管理:通过unix io接口管理

8.2 简述Unix IO接口及其函数

1. Unix/IO接口:

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

STDIN_FILENO、

STDOUT_FILENO 、

STDERR_FILENO,

它们可用来代替显式的描述符值。

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

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

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

2. Unix/IO函数:

(1). open:打开文件

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open(char *filename, int flags, mode_t mode);

// 返回:若成功则为新文件描述符,若出错为 -1。

flags 参数指明了进程打算如何访问这个文件:

O_RDONLY:只读。

O_WRONLY:只写。

O_RDWR:可读可写。

mode 参数指定了新文件的访问权限位

(2). close:关闭文件

#include <unistd.h>

int close(int fd);

// 返回:若成功则为 0,若出错则为 -1

(3). 读写文件

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t n);

// 返回:若成功则为读的字节数,若 EOF 则为0,若出错为 -1。

ssize_t write(int fd, const void *buf, size_t n);

// 返回:若成功则为写的字节数,若出错则为 -1。

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

write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置。图 10-3 展示了一个程序使用 read 和 write 调用一次一个字节地从标准输入复制到标准输出。

8.3 printf的实现分析

printf函数为va_list arg = (va_list)((char*)(&fmt) + 4);

typedef char *va_list是一个字符指针。(char*)(&fmt) + 4) 表示的是...中的第一个参数。因为 fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。

(1)从vsprintf生成显示信息,

i = vsprintf(buf, fmt, arg); vsprintf返回的是要打印出来的字符串的长度。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

(2)write系统函数

write(buf, i);write函数的功能就是执行一个写操作。以我们学过的知识可知,写操作是计算机的底层操作,是对计算机硬件进行的操作。通过中断门,来实现特定的系统服务。

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

陷阱-系统调用

init_idt_desc(INT_VECTOR_SYS_CALL,DA_386IGate,sys_call,PRIVILEGE_USER);调用sys_call显示格式化了的字符串。

字符显示驱动子程序

从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

(6)显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

要分析他的实现与作用,我们需要首先来看看geychar()的函数体

Getchar()函数的作用是从stdin流中读入一个字符

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求。键盘中断服务程序先从键盘接口取得按键的扫描码,然后根据其扫描码判断用户所按的键并作相应的处理,最后通知中断控制器本次中断结束并实现中断返回。

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

8.5本章小结

输入输出看似简单,实际是一个非常精巧的过程,从程序发出请求到系统函数调用到设备相应,需要执行许多步骤,往往也是拖慢程序的主要因素和一些崩溃异常的高发地,需要谨慎选用函数、命令实现目的。

结论

到这里Hello的整个生命进程就结束了,借助操作系统的帮助,连一点痕迹都没有留下。下面我们来分析一下Hello.c是如何一步一步变成可执行的程序,并最终让我们在命令行看到他的:

  1. 首先,通过文本编辑器等,将hello.c的内容写入文件
  2. 然后经过gcc这个强大的编译器
    1. 预处理,生成完成的c程序文件hello.i
    2. 编译,生成汇编语言描述的文件hello.s
    3. 汇编,将汇编语言文件转化为可重定位目标文件hello.o
    4. 链接,添加必要的模块和函数库,连接成可以直接复制到内存中执行的可执行文件hello
  3. 命令行中输入./hello调用hello文件时,shell会先调用fork创建子进程,并在其中由 execve.创建虚拟内存空间映射、将虚拟内存地址翻译为物理地址、调用高速缓存与缺页处理将hello加载进内存与cpu
  4. cpu取到指令后,控制 hello的逻辑流进行运行,其间调用printfgetchar等函数调用IO设备,进行屏幕的显示和键盘读入
  5. 异常处理:对于运行程序时键盘输入的ctrl-cctrl-z等指令系统中断并调用相应的信号处理程序进行处理

回顾hello的完整执行流程,我了解了现代计算机的各种先进的硬件和精妙的设计思想,知道了在程序设计之外还有这么广阔的计算机系统的世界,加深了对计算机学习的兴趣。

附件

  1. Hello.c:源程序
  2. Hello.i:预处理生成文件
  3. Hello.s:编译生成文件
  4. Hello.o:汇编生成文件
  5. Hello.elf:hello.o的elf格式
  6. Hello.asm:hello.o的反汇编文件
  7. Hello:链接生成的可执行文件
  8. Hello_exe.elf:hello的elf格式
  9. Hello_exe.asm:hello的反汇编文件
  10. Test.c:在预处理阶段进行对头文件展开的验证
  11. Test.i:test.c的预处理文件
  12. Rtest.c:作为对照组参考
  13. Rtest.i:rtest.c的预处理文件

参考文献

[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] HIT计算机系统(CSAPP)大作业_使用objdump工具(objdump -h)查看未装入内存之前的可执行文件中main函数的地址,比-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值