程序人生--hello的p2p

计算机系统

大作业

题     目  程序人生-Hello’s P2P   

专       业   网络空间安全                    

学     号   2022112149                    

班     级   2203901                    

学       生   段宇轩                  

指 导 教 师   史先俊                    

计算机科学与技术学院

2024年5月

摘  要

本文以一个简单的hello.c程序开始,介绍了一个程序包括预处理、编译、汇编、链接、进程管理、存储管理、I/O管理这几部分运行的完整生命周期,详细介绍了程序从被键盘输入、保存到磁盘,直到最后程序运行结束变为僵尸进程的过程。

关键词:预处理,编译,汇编,链接,进程,存储管理,IO                        

目  录

第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 本章小结... - 7 -

第3章 编译... - 8 -

3.1 编译的概念与作用... - 8 -

3.2 在Ubuntu下编译的命令... - 8 -

3.3 Hello的编译结果解析... - 8 -

3.4 本章小结... - 12 -

第4章 汇编... - 13 -

4.1 汇编的概念与作用... - 13 -

4.2 在Ubuntu下汇编的命令... - 13 -

4.3 可重定位目标elf格式... - 13 -

4.4 Hello.o的结果解析... - 16 -

4.5 本章小结... - 18 -

第5章 链接... - 19 -

5.1 链接的概念与作用... - 19 -

5.2 在Ubuntu下链接的命令... - 19 -

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

5.4 hello的虚拟地址空间... - 21 -

5.5 链接的重定位过程分析... - 22 -

5.6 hello的执行流程... - 25 -

5.7 Hello的动态链接分析... - 25 -

5.8 本章小结... - 25 -

第6章 hello进程管理... - 27 -

6.1 进程的概念与作用... - 27 -

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

6.3 Hello的fork进程创建过程... - 27 -

6.4 Hello的execve过程... - 27 -

6.5 Hello的进程执行... - 28 -

6.6 hello的异常与信号处理... - 28 -

6.7本章小结... - 31 -

第7章 hello的存储管理... - 32 -

7.1 hello的存储器地址空间... - 32 -

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

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

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

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

7.6 hello进程fork时的内存映射... - 34 -

7.7 hello进程execve时的内存映射... - 35 -

7.8 缺页故障与缺页中断处理... - 35 -

7.9动态存储分配管理... - 35 -

7.10本章小结... - 36 -

第8章 hello的IO管理... - 37 -

8.1 Linux的IO设备管理方法... - 37 -

8.2 简述Unix IO接口及其函数... - 37 -

8.3 printf的实现分析... - 38 -

8.4 getchar的实现分析... - 38 -

8.5本章小结... - 39 -

结论... - 39 -

附件... - 40 -

参考文献... - 41 -

第1章 概述

1.1 Hello简介

P2P是From Program to Process的过程,hello的一生是从C语言程序开始的,首先经过cpp进行预处理,生成文本文件hello.i,然后经过编译器ccl生成hello.s汇编程序,接着经过汇编器as生成hello.o,最后经过链接器ld,生成可执行文件hello。同时系统创建一个进程把程序内容加载,实现程序到进程的转化。O2O是From Zero-0 to Zero-0的过程,运行前shell首先调用execve函数将hello程序加载到相应的上下文中,将程序内容载入物理内存,然后调用main函数。运行结束后,父进程回收进程释放虚拟内存空间,hello的内容也被删除。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:Windows11 64位;Vmware 11;Ubuntu 16.04 LTS 64位;

开发和调试工具:Visual Studio 2022 64位;CodeBlocks 64位;gdb;readelf;objdump

1.3 中间结果

hello.i:hello.c预处理后的文件

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

hello.o:hello.s汇编后的文件

hello:hello.o链接后的文件

hellooasm.txt:hello.o反汇编后代码

helloasm.txt:hello反汇编后代码

hello.o.elf:hello.o的elf格式

hello.elf:hello的elf格式

1.4 本章小结

本章简述了hello程序的一生,概括了从P2P到020的整个过程

第2章 预处理

2.1 预处理的概念与作用

概念:在C语言中,预处理阶段是代码执行之前的一个重要步骤,负责对源代码进行宏替换、条件编译等处理。

作用:在编译和链接之前利用预处理指令对程序进行预处理可以对源文件进行一些操作,比如文本替换、文件包含、删除部分代码等。以#开头的代码段即为预处理工作的对象,包括#include等,能够把包含的源文件加入到当前位置,还可以替换由#define定义的东西,包括数值宏常量,宏定义表达式等,同时还可以进行去注释把注释删掉

2.2在Ubuntu下预处理的命令

图2.1 在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

生成的hello.i内容如下

                       图2.2 生成的hello.i内容

图2.3 生成的hello.i内容

可以看到main函数前增加了很多代码,这是由于预处理后生成的文件前面会有#开头的预处理指令#include <stdio.h>、#include <stdlib.h>等相应的源文件内容以及宏替换的内容等。main函数未进行处理内容不变。

2.4 本章小结

本章介绍了预处理文件与源文件的区别,展示了预处理的作用

第3章 编译

3.1 编译的概念与作用

概念:编译是把通常为高级语言的源代码(hello.i)到能直接被计算机执行的目标代码(hello.s)的翻译过程

作用:将高级语言书写的源程序转换为机器指令,使机器更容易理解,为汇编做准备。   

3.2 在Ubuntu下编译的命令

                     图3.1 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

生成的hello.s如下

图3.2 hello.s

接下来逐步分析各种操作

3.3.1数据

包括常量、变量(全局/局部/静态)、表达式、类型、宏,hello程序中只有常量和局部变量

字符串常量如图所示

图3.3 字符串常量

可以看到LC1里存放了"Hello %s %s\n"这个字符串,字符串"用法: Hello 学号 姓名 秒数!\n"存在了LC0处,汉字全部被替换成了“\三个数字”的形式

数字常量如图所示

图3.4 数字常量

$后面加上数字表示立即数,如图,这个数就是5

局部变量被存在寄存器里或栈上,这里无法看出

3.3.2赋值

包括 =   ,逗号操作符(没用到),赋初值/不赋初值

图3.5 赋值

这里就是一个赋值操作,将值0赋在-4(%rbp)里

3.3.3类型转换(隐式或显式)

本程序中没有

3.3.4算术操作

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

本程序里只有+,如图所示

图3.6 +操作

意为把-4(%rbp)加上1再存回-4(%rbp)

3.3.5逻辑/位操作:

本程序中没有

3.3.6关系操作

包括==    !=      >    <      >=     <=

程序中一个关系操作如下

图3.7 关系操作

意思是如果-4(%rbp)内的值小于等于9,就跳转到L4,否则继续向下执行

3.3.7数组/指针/结构操作

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

图3.8 数组操作

这张图可以看到数组操作,对应的程序代码应该是printf("Hello %s %s %s\n",argv[1],argv[2],argv[3])以数组首地址为基址,使用变址寻址方式实现对数组元素的表示,L4中的第一条语句将数组的首地址放到了rax里,将首地址表示为argv,第二条语句访问argv+24处的内容,是argv[3],第五条语句访问a+16处的内容,应为argv[2],第八条语句访问argv+8的内容,应为argv[1]

3.3.8函数操作

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

同3.3.7的图,可以看到有printf函数的调用,使用了call指令。call执行的过程首先把下一条指令地址压入栈中作为返回地址,然后更新地址为call后面的符号表示的地址。通过ret返回时把地址重新设置为栈中之前存放的指令地址值

准备printf的参数时,从上面的指令1可以看出将argv[3]放在rcx里,argv[2]放在rdx里,argv[1]放在rsi里,然后将格式字符串放在rdi里,这样就准备好了printf的全部参数

3.3.9控制转移

如if/else switch for while  do/while  ?:       continue  break

本程序中有if(argc!=5)和for(i=0;i<10;i++)两个判断,对应的指令段分别如下

图3.9 对应的指令段

这两条语句将argc和5进行比较,如果相等,则if(argc!=5)中的条件不成立,跳转到L2,否则继续

图3.10 比较

这两条语句将i和9进行比较,如果i<=9,则i跳转到L4循环体,否则结束循环。

3.4 本章小结

本章介绍了汇编指令,查看了程序的的机器级实现。我们可以发现汇编指令和C语言代码语句之间存在对应关系,因此可以根据汇编代码进行反汇编,观察C程序的结构

第4章 汇编

4.1 汇编的概念与作用

概念:汇编是指把汇编语言翻译成机器语言的过程。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。

作用:将汇编程序翻译为机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件中

4.2 在Ubuntu下汇编的命令

图4.1 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

首先输入readelf -a hello.o > hello.o.elf

生成的elf文件如下

图4.2 elf文件

后面还有一段重定位节

图4.3 重定位节

可以看到elf可重定位目标文件格式组成如下

elf头

程序头表

.text 节

.rodata节

.bss节

.symtab节

.rel.txt节

.rel.data节

.debug节

节头表

elf头包含了字大小、字节序,文件类型、机器类型、节头表的位置、条目大小、数量等,程序头表包含了页面大小,虚拟地址内存段,段大小,.text节包含已编译程序的机器代码,.rodata节包含只读数据,.data节包含已初始化全局和静态变量,.bss节包含未初始化/初始化为0的全局和静态变量,.symtab节是符号表,包含函数和全局/静态变量名,节名称和位置,.rel.text节是可重定位代码,.text节是可重定位信息,包括在可执行文件中需要修改的指令地址,需修改的指令,.rel.data 节是可重定位数据,包括data节的可重定位信息,在合并后的可执行文件中需要修改的指针,数据的地址,.debug节为符号调试的信息,节头表包括每个节的在文件中的偏移量、大小等。

汇编器的重定位条目告诉链接器在将目标文件合并成可执行文件时如何修改。代码的重定位条目放在.rel.text中,数据的重定位条目放在.rel.data。重定位条目包含以下几个部分:需要被修改引用的节偏移,被修改引用应该指向的符号,类型等。

4.4 Hello.o的结果解析

输入objdump -d -r hello.o > hellooasm.txt后,生成了反汇编文件如下

图4.4 反汇编文件

可以看到左侧是二进制的机器语言,右侧是汇编程序,每一行的16进制数表示一条机器指令,对应汇编语言中的一行。机器语言中所有的操作数都是16进制的。机器语言中call后面跟的是该函数的地址,而汇编语言中call后面跟的是函数名。机器语言中跳转的目标是地址,而汇编语言中跳转的目标是标号。

4.5 本章小结

本章介绍了汇编的概念以及汇编的作用,分析了hello.o的ELF格式,同时对hello.o反汇编得到了反汇编程序,并分析了该反汇编程序与汇编语言程序hello.s中语句的对应关系。

5章 链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合为一个单一文件的过程,所得到的文件可以被加载到内存并执行。链接由链接器程序执行

作用:将程序调用的各种静态链接库和动态链接库整合到一起,完善重定位目录,使之成为一个可运行的程序

5.2 在Ubuntu下链接的命令

图5.1 在Ubuntu下链接的命令

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

首先输入readelf -a hello > hello.elf

得到的结果如下

图5.2 elf文件

图5.3 elf文件

可以看到hello的elf与hello.o的elf都包含elf头,程序头表,.text节,.rodata节,.bss节,.symtab节,.debug节,节头表,不同点是hello里没有.rel.txt节和.rel.data节,在链接的过程中他们已经完成了重定位。同时也新增了重定位信息.rela.dyn,还新增了.init节等

5.4 hello的虚拟地址空间

使用edb加载hello查看本进程的虚拟地址空间各段信息,结果如下图所示

图5.4 虚拟地址空间各段信息

可以看到hello可执行部分起始地址为0x401000,在5.3中可以看到.init节的起始地址也是0x401000,二者之间对应

图5.5 elf文件中信息

5.5 链接的重定位过程分析

输入objdump -d -r hello >helloasm.txt,得到反汇编文件如下

图5.6 反汇编文件

图5.7 反汇编文件

首先可以看到导入了共享库,导致多了很多代码相比于hello.o,同时,hello.o中的可重定位条目在hello中都已经被定位,相应的机器代码被修改,main中每条数据和指令都已经确定好了虚拟地址,不再是hello.o中的偏移量,在hello.o中跳转指令和call指令后为绝对地址,而在hello中已经是重定位之后的虚拟地址。比如下面两幅图的地址对比

图5.8 hello.o中结果

图5.9 hello中结果

下面具体分析重定位的过程,以401145处的call指令为例子

图5.10 call指令

上图是hello.o中的对应指令,可以看到调用了puts,在hello中找到puts的地址为0x401090如下图所示

图5.11 找到puts的地址

当前地址为call指令的下一条指令的地址,也就是0x40114a。而我们要跳转到的地方为0x401090,差0xba,因此需要减去0xba,补码是0xff ff ff 46,由于是小端法,因此重定位目标处应该填入46 ff ff ff。

5.6 hello的执行流程

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

hello!_start  0x00000000004010f0

hello!__libc_csu_init       0x00000000004011d0

hello!_init   0x0000000000401000

hello!main   0x0000000000401125

hello!printf@plt 0x00000000004010a0

hello!atoi@plt    0x00000000004010c0

hello!sleep@plt  0x00000000004010e0

hello!getchar@plt     0x00000000004010b0

hello!exit@plt    0x00000000004010d0

hello!_fini   0x0000000000401248

5.7 Hello的动态链接分析

动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,将过程地址的绑定推迟到第一次调用该过程时,为了举例子可以首先找到.got的地址为0x403ff0

图5.12 .got的地址

然后在edb中找到相应的地址前后内容如下

图5.13 edb中地址

调用dl_init后PLT的内容发生了改变,动态链接之前0x401000地址存的是INIT的虚拟地址,而动态链接后变成了实际地址

5.8 本章小结

本章展现了由可重定位目标文件链接出可执行文件的过程,分析了可执行文件的elf格式,对比了可执行文件和可重定位目标文件的区别,并分析呈现了重定位过程如何进行,同时利用edb工具逐步执行hello程序,阐述整个程序的执行,分析动态链接过程

6章 hello进程管理

6.1 进程的概念与作用

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

作用:进程给程序提供了一个独立的逻辑控制流,提程序独占使用处理器的假象;进程给程序提供了一个私有的地址空间,提供了程序独占使用内存系统的假象。

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

作用:Shell是一种交互型程序,用于代表用户运行其他程序

处理流程:Shell执行一系列的读/求值步骤。读步骤读取用户的命令行,求值步骤解析命令,代表用户运行。Shell在求值一个命令行时会创建一个进程组。一个进程组对应一个作业,作业是为了求这个命令行的值而创建的进程。

6.3 Hello的fork进程创建过程

首先,系统级父进程init调用fork()函数能够创建子进程,子进程能够共享父进程的文件资源,在init中由于init是整个系统进程,所以可以看成创建的子进程为单独一个进程,且占用整个内存。fork()会返回两个值,在父进程中返回子进程pid,在子进程中返回0

6.4 Hello的execve过程

shell fork一个子进程后,execve函数在当前进程的上下文中加载并运行一个新程序即hello。execve需要三个参数:可执行目标文件名finename、参数列表argv、环境变量列表envp。这些都由shell构造并传递。除非找不到filename,否则execve不会返回。调用execve会将这个进程执行的原本的程序完全替换,它会删除已存在的用户区域,包括数据和代码;然后映射私有区,为hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的;之后映射共享区;最后把控制传递给当前的进程的程序入口

6.5 Hello的进程执行

进程轮流使用CPU是通过上下文切换、进程调度、用户模式和内核模式这些机制来实现的。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策叫做调度,是由内核中称为调度器的程序处理的。内核调度了一个新的进程运行后,它就抢占当前进程,并使用上下文切换机制将控制转移到新的进程,上下文切换(1)保存当前进程的上下文(2)恢复某个先前被抢占的进程被保存的上下文(3)将控制传递给这个新恢复的进程。上下文切换机制建立低层的异常机制上,需要在内核模式下进行。当设置了模式位时,进程就运行在内核模式中,表示它可以执行指令集中的任何指令并且可以访问系统中的任何内存位置。进程从用户模式变为内核模式的唯一方法是通过中断、故障或陷入系统调用这样的异常。当某个异常发生时,系统进入内核模式,内核可以执行从某个进程A到进程B的上下文切换,在切换的第一部分,内核代表进程A在内核模式下执行指令,在某一时刻开始代表进程B在内核模式下执行指令。切换完成后,内核代表进程B在用户模式下执行指令。这样就实现了多任务。

6.6 hello的异常与信号处理

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

中断:中断是来自处理器外部的I/O设备的信号的结果。

陷阱:陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。

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

终止:终止是不可恢复的致命错误造成的结果。终止处理程序从不将控制返回给应用程序。

正常运行时,输出了10次结果,再输入一个字符并回车后退出,结果如下

6.1 正常运行

乱按,回车没有产生影响,结果如下

6.2 乱按回车

按下ctrl-c后,触发一个中断异常,内核向shell发送SIGINT信号,前台进程组的成员终止

                             6.3 终止

按下ctrl-z后,触发一个中断异常,内核向shell发送SIGTSTP信号,前台进程组的成员停止

                              6.4 停止

输入jobs,可以看到hello进程已经停止

6.5 输入jobs

输入ps,可以看到hello进程仍存在

6.6 输入ps

输入pstree,所有进程以树的形式显示

6.7 输入pstree

输入fg,hello进程回到前台接着上次被中断的位置继续执行

6.8 输入fg

输入kill指令后,向hello发送一个SIGKILL信号,进程终止。

6.9 输入kill

6.7本章小结

本章介绍了hello进程在运行中的过程。首先是介绍了系统使用shell-bash来解读键盘指令从而对系统进行操作的一个过程。其次介绍了主进程init如何通过fork和execve函数调用生成一个子进程的过程。并且介绍了进程在计算机中处理的一些内容。最后介绍了进程中的信号处理,介绍了异常控制流在进程中的作用

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是指由程序产生的与段相关的偏移地址部分。在C程序中使用&操作读取指针变量的值就是逻辑地址,它是相对于当前进程数据段的地址。一个逻辑地址由两部份组成:段标识符和段内偏移量。

线性地址:线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,即段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了页式管理,那么线性地址可以再变换产生物理地址。若没有启用页式管理,那么线性地址直接就是物理地址。

虚拟地址:因为虚拟内存空间的概念与逻辑地址类似,因此虚拟地址和逻辑地址实际上是一样的,都与实际物理内存容量无关。

物理地址:存储器中的每一个字节都有一个唯一的存储器地址,这个存储器地址称为物理地址,又叫实际地址或绝对地址,是地址总线上实际传输的地址。

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

逻辑地址由两部分组成:段标识符,段内偏移量。段标识符是一个16位长的字段。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。索引号就是“段描述符”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。每一个段描述符由8个字节组成。全局的段描述符,放在全局段描述符表(GDT)中,一些局部的段描述符,放在局部段描述符表(LDT)中。Linux通过分段机制,将逻辑地址转化为线性地址。给定一个完整的逻辑地址。首先,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。然后,拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,基地址就知道了。最后基地址加偏移量就是要转换的线性地址

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

线性地址到物理地址之间的转换通过使用分页机制完成。分页机制是指将虚拟内存分割为虚拟页的大小固定的块。为判断虚拟页是否缓存在DRAM的某个地方,并确定虚拟页所在的物理页位置,因此需要一个页表。页表是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个n位地址字段组成,有效位的设置与否表明了虚拟页是否被DRAM缓存。在地址翻译过程中,需要用到一个页表基址寄存器,指向当前页表。一个n位虚拟地址包含两个部分,一是p位的虚拟页面偏移,二是一个n-p位的虚拟页号。在虚拟页已被缓存的情况下,页面命中,MMU利用VPN来选择合适的PTE,将页表条目中的物理页号和虚拟地址中的虚拟页面偏移结合起来就可以得到一个物理地址。而如果虚拟页没有被缓存,那么就需要处理缺页,触发缺页异常,并将控制传递到缺页异常处理程序,缺页处理程序能够确定物理内存中的牺牲页,进而判断是否因被修改过而需要调出内存,完成牺牲页的替换之后,则调入新的页面,并更新内存中的PTE,最后返回到原来进程,将引起缺页的虚拟地址重新发给MMU,这次便变成了命中的情况。

7.1 页式管理

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

由于每次访问PTE都需要一定的时间,并且每次最近访问的PTE和存储器层次结构中其他地方一样具有局部性,因此可以设立一个页表的缓存以加速地址翻译。TLB(翻译后备缓冲器)是MMU中一个小的具有高相联度的集合,其每一行都保存着一个单个PTE组成的块,最常用的PTE可以缓存在TLB中,这样就省去了每次访问PTE需要访问L1甚至内存的时间开销。为了压缩页表大小,可以使用多级页表。在多级页表中,一级页表中每个PTE映射虚拟存储空间中一个由很多虚拟页组成的片,如果该片中至少有一个虚拟页是分配了的,那么一级页表的PTE指向一个二级页表的基址,二级页表中每个PTE再映射一个小一点的片,直到第k级页表的PTE存储的是PPN。因为一级页表项为空,则对应的二级页表就不存在(不用保存);只有一级页表需要常驻在主存中,二级页表根据需要,创建、页面调入或调出。这样就节省了用于存储页表的空间。在实际访问时,首先访问TLB,如果TLB里缓存了要访问的PTE则可以直接从这个PTE里拼接得到物理地址。如果TLB里没有缓存要访问的PTE,则需要依次访问一级页表,二级页表,三级页表,四级页表。

7.2 多级页表

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

与TLB相似,利用局部性原理,采用组相联的方式,存储一段时间内所加载的地址附近的内容。在得到物理地址后,先从L1 cache中找,没有再从L2 cache中找,然后L3 cache,然后主存。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效替代了当前程序。加载并运行a.out首先要删除进程虚拟地址中已存在的用户区域,然后为新程序的代码、数据和栈区域创建新的区域结构。这些区域都是私有的、写时复制的。代码和数据区域被映射成a.out文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。如果a.out程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。execve做的最后一件事就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

当要访问的页没有缓存在主存里时,MMU就会触发一个缺页故障。首先处理器生成一个虚拟地址,并将其传送给MMU。MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE。高速缓存/主存向MMU返回PTE,PTE的有效位为零, 因此 MMU 触发缺页异常。缺页处理程序确定物理内存中的牺牲页,缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

分配器有两种基本风格:

(1)显式分配器:要求应用显式地释放任何分配的块,例如C标准库提供的malloc程序包。

(2)隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就是放这个块,也被称为垃圾收集器。

7.10本章小结

本章主要介绍了hello的存储器地址空间,逻辑地址到线性地址、线性地址到物理地址的变换,接着介绍了四级页表下的线性地址到物理地址的变换,介绍了hello的内存映射,及缺页故障与缺页中断处理和动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m个字节的序列,所有的I/O设备都被模型化为文件,所有的输入输出都被当作是文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,被称为是Unix I/O,这使得所有的输入和输出都能够以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

接口:一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。Shell创建的每个进程都有三个打开的文件:标准输入、标准输出、标准错误。对于每个打开的文件,内核保持着一个文件位 置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。一个读操作就是从文件复制 n > 0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k + n。给定一个大小为 m 字节的文件,当 k >= m 时,执行读操作会触发 EOF,应用程序能检测到它。类似地,写操作就是从内存中复制 n > 0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

函数:

(1) open函数:int open(char *filename,int flags,mode_t node);

将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

(2) close函数:int close(int fd);

关闭一个打开的文件,当关闭已关闭的描述符会出错。

(3) read函数:ssize_t read(int fd,void *buf,size_t n);

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

(4) write函数:ssize_t write(int fd,const void *buf,size_t n);

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

8.3 printf的实现分析

实现如下:

int printf(const char *fmt, ...)

{

   int i;

   char buf[256];

   va_list arg = (va_list)((char*)(&fmt) + 4);

   i = vsprintf(buf, fmt, arg);

   write(buf, i);

   return i;

}

vsprintf函数将所有的参数内容格式化之后存入buf,返回格式化数组的长度。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.字符显示驱动子程序:从ASCII到字模库到显示vram。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。

8.4 getchar的实现分析

实现如下:

int getchar(void)

{

    static char buf[BUFSIZ];

    static char *bb = buf;

static int n = 0;

if(n==0)

{

    n = read(0,buf,BUFSIZ):

    bb = buf;

}

Return(--n>=0)?(unsigned char) *bb ++ : EOF;

}

getchar函数调用了read函数,通过系统调用read读取存储在键盘缓冲区的ASCII码,直到读到回车符才返回。不过read函数每次会把所有内容读进缓冲区,如果缓冲区本来非空,则不会调用read函数,而是简单的返回缓冲区最前面的元素。处理异步异常时,用键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

8.5本章小结

本章主要介绍了Linux的I/O设备管理方法、Unix IO接口及其函数,并分了printf函数和getchar函数的实现。

结论

从程序员编写hello.c的源程序开始,hello开始了它精彩的一生。出生后它首先经历了预处理,将c文件调用的库与原本的c文件进行合并,得到hello.i文本文件,之后经历了编译生成汇编语言文件hello.s,然后hello.s汇编得到二进制可重定位目标文件hello.o,再进行链接使hello.o与其它的可重定位目标文件和动态链接库链接生成可执行文件hello,hello至此长大成人,可以被加载入内存并运行。

hello长大成人后,首先cs为它创建了进程,终端shell调用fork函数,创建一个子进程,为它的加载运行提供虚拟内存空间等上下文,之后,shell调用execve函数,启动加载器映射虚拟内存,之后开始载入物理内存,再进入main函数。然后通过TLB和多级页表,实现虚拟内存和物理内存的翻译,进而访问计算机的存储结构,访问内存。Hello还需要输入输出与外界进行交互,IO系统帮他实现了这一点。

最后,hello的一生即将结束,hello.c程序被回收,重新进入硬盘。虽然hello的一生如此短暂,但是却非常精彩!

学习完计算机系统这门课程让我对计算机的运行原理有了更深入的了解。通过学习计算机系统的课程,我对计算机硬件和软件之间是如何协同工作的有了更清晰的认识,了解了计算机是如何从底层的硬件开始执行指令的。在课程中,我学习了计算机的体系结构、指令集、处理器设计、内存管理、输入输出系统等内容,这些知识帮助我更好地理解计算机系统的运作原理。

通过实验,我深入学习了如何编写汇编语言程序、理解操作系统的工作原理以及进行系统级编程。这些实践让我不仅仅停留在理论层面,还能够将所学知识应用到实际中,从而加深对计算机系统的理解。

学习计算机系统这门课程让我意识到了计算机科学的广度和深度,也让我更加珍惜每一个软件系统背后的复杂性和技术创新。这门课程不仅增强了我的计算机科学基础,还培养了我解决问题和分析系统的能力。今后我会继续加强计算机相关课程的学习。

附件

hello.i:hello.c预处理后的文件

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

hello.o:hello.s汇编后的文件

hello:hello.o链接后的文件

hellooasm.txt:hello.o反汇编后代码

helloasm.txt:hello反汇编后代码

hello.o.elf:hello.o的elf格式

hello.elf:hello的elf格式

参考文献

[1] edb的安装以及带参数使用.原文链接: https://blog.csdn.net/weixin_42781851/article/details/89289049

[2] ELF文件的加载和动态链接过程.原文链接:https://www.iteye.com/blog/jzhihui-1447570

[3]  《深入理解计算机系统》 Computer Systems A progammer’s Perspective Third Edition.机械工业出版社

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值