【2022】哈工大计算机系统大作业——程序人生Hello’s P2P

2022哈工大计算机系统大作业——程序人生Hello’s P2P


摘要

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

关键词:Liunx;P2P;O2O;计算机系统;hello


第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.c的O2O过程。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows10 64位;VMware Workstation Pro15.5.1;Ubuntu 20.04.4
开发和调试工具:gdb;edb;readelf;objdump;Code::Blocks20.03

1.3 中间结果

hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。
hello_elf:hello用readelf -a hello指令生成的文件。

1.4 本章小结

本章根据hello的自白,概括介绍了hello的P2P和O2O的过程。此外,还介绍了本实验用到的硬软件环境和开发调试工具。

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念:

当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预处理指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。

2.1.2预处理的作用:

所有的预处理器(cpp)命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
(1)添加对应的头文件在#include处
(2)删除#define并展开对应的宏,#undef取消已经定义的宏。
(3)#ifdef, 若宏已经定义,则返回真;#ifndef, 若宏没有定义,则返回真
(4)处理所有的条件预编译指令,例如#if#endif,根据“#if”后面的条件决定需要编译的代码。
(5) #if,如果给定条件为真,则编译下面代码;#else是#if 的替代方案;#elif,如果前面的#if给定条件不为真,当前条件为真,则编译下面代码;#endif,结束一个 #if……#else 条件编译块。
(6)#error,当遇到标准错误时,输出错误信息。
(7)#pragma,使用标准化方法,发布特殊的命令到编译器中。

2.2在Ubuntu下预处理的命令

打开终端,输入gcc –E hello.c –o hello.i 或 cpp hello.c > hello.i ,即可生成文本文件hello.i。
图2-2-1 Ubuntu下预处理指令gcc及结果
图2-2-1 Ubuntu下预处理指令gcc及结果
图2-2-2 Ubuntu下预处理指令cpp及结果
图2-2-2 Ubuntu下预处理指令cpp及结果

2.3 Hello的预处理结果解析

打开hello.i程序,程序是可读的文本文件,总共有3060行。观察发现,其中的注释已经消失,前一部分的代码为,被加载到程序中的头文件;程序的最后一部分与hello.c中的main函数完全相同。
在这里插入图片描述
图2-3-1 hello.c预处理结果

2.4 本章小结

本章首先介绍了预处理的概念与作用,接着以hello.c为例,演示了在Ubuntu下如何预处理程序,并对结果进行分析。

第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念:

编译器(cc1)将预处理之后生成的文本文件hello.i翻译成文本文件hello.s,它通常包含一个汇编语言程序。

3.1.2编译的作用:

将高级语言书写的源程序转换为一条条机器指令,机器指令和汇编指令一一对应,使机器更容易理解,为汇编做准备。

3.2 在Ubuntu下编译的命令

打开终端,输入cc1 hello.i -o hello.s 或 gcc -S hello.c -o hello.s
(cc1找不到用/usr/lib/gcc/x86_64-linux-gnu/9/cc1 hello.i -o hello.s)
在这里插入图片描述
图3-2-1 Ubuntu下编译cc1
在这里插入图片描述
图3-2-2 Ubuntu下编译gcc

3.3 Hello的编译结果解析

编译过程是整个过程构建的核心部分,编译成功,源代码会从文本形式转换为机器语言。
下面是hello.s汇编文件内容:

3.3.1对文件信息的记录

在这里插入图片描述
图3-3-1 hello.s文件内容

首先是记录文件相关信息的汇编代码,为之后链接过程使用。其中.file表明了源文件,.text代码段,.section .radata只读代码段,.align对齐方式为8字节对齐,.string字符串,.global全局变量,.type声明main是函数类型。

3.3.2对局部变量的操作

在这里插入图片描述
图3-3-2-1 hello.s文件内容
在这里插入图片描述
图3-3-2-2 hello.s文件内容

局部变量存储在栈中,当进入函数main的时候,会根据局部变量的需求,在栈上申请一段空间供局部变量使用。当局部变量的生命周期结束后,会在栈上释放。

在hello.c中i是局部变量,在hello.s中可以看到,首先跳转到了.L3的位置,然后将栈指针减少4,即存储局部变量i,然后跳转到.L4进行接下来的操作。

3.3.3对字符串常量的操作

在main函数前,在.rodata处的.LC0和.LC1已经存储了字符串常量,标记该位置是代码是只读的。在main函数中使用字符串时,得到字符串的首地址,如下图红框。
在这里插入图片描述
图3-3-3 hello.s文件内容

3.3.4对立即数的操作

立即数直接用$加数字表示
在这里插入图片描述

图3-3-4 hello.s文件内容

3.3.5 参数传递------对main的参数argv的传递

在main函数的开始部分,因为后面还会使用到%rbp数组,所以先将%rbp压栈保存起来。21行将栈指针减少32位,然后分别将%rdi和%rsi的值存入栈中。
由此我们知道,%rbp-20和%rbp-32的位置分别存了argv数组和argc的值。
在这里插入图片描述

图3-3-5 hello.s文件内容

3.3.6 数组的操作

对数组的操作,都是先找到数组的首地址,然后加上偏移量即可。例如在main中,调用了argv[1]和argv[2],在汇编代码中,每次将%rbp-32的的值即数组首地址传%rax,然后将%rax分别加上偏移量16和8,得到了argv[1]和argv[2],在分别存入对应的寄存器%rsi和%rdx作为第二个参数和第三个参数,之后调用printf函数时使用。
在这里插入图片描述
图3-3-6 hello.s文件内容
调用完printf后,同样在偏移量为24时,取得argv[3]并存入%rdi作为第一个参数在调用函数atoi使用。

3.3.7对函数的调用与返回

函数的前六个参数有寄存器传参,返回值存在%rax寄存器中。在函数调用时,先将相应的值存入相应的寄存器,然后使用call指令调用函数和ret指令返回函数。注意,由于函数是公用一套寄存器的,在调用一个函数之前,要先将当前函数的一些值保存起来,调用完再恢复。
对printf函数的调用,再3.3.6中已经介绍过,取得argv数组的第二个和第三个元素放入寄存器%rsi和%rdx,然后41行取得了字符串的地址,并存入了2%rdi中作为第一个参数,这样三个参数都准备好后,用call指令调用了printf函数。
在这里插入图片描述
图3-3-7-1 hello.s文件内容
对atoi函数和sleep函数的调用,先取得argv存入%rdi作为第一个参数,然后第48行call指令调用了atoi函数,接着atoi的返回值存入了%rax中,再将其存入%rdi中作为sleep的第一个参数,然后用call调用sleep函数。
在这里插入图片描述
图3-3-7-2 hello.s文件内容

3.3.9 for循环

对于for循环,将循环变量存入一个寄存器中,然后当执行完一个循环体之后,更新循环变量(一般是用add指令进行自增),然后用cmp指令将其与条件进行比较,满足则继续,否则退出循环。
在这里插入图片描述
图3-3-9 hello.s文件内容

3.3.10 赋值操作

赋值操作很简单,用movq指令即可,例如将a寄存器的值赋值给b寄存器,用movq a b(以8字节为例)。
在hello.s中很多地方都用到了赋值语句,比如说对局部变量i的赋值:
在这里插入图片描述
图3-3-10 hello.s文件内容

3.4 本章小结

本章首先介绍了编译的概念和作用,然后在Ubuntu下以hello.s为例,通过分析其汇编程序,理解编译器是如何处理各种数据类型和各类操作的。编译是从高级语言程序生成可执行文件的过程中的关键一步。

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编器(as)将汇编程序翻译为机器语言指令,然后把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件中(该文件是个二进制文件,文本编译器打开会乱码)。

4.1.2汇编的作用

生成机器指令,方便机器直接分析。

4.2 在Ubuntu下汇编的命令

打开终端输入 as hello.s -o hello.o 或 gcc -c hello.s -o hello.o
在这里插入图片描述
图4-2-1 Ubuntu下汇编as和gcc

在这里插入图片描述
图4-2-2汇编生成hello.o文件

4.3 可重定位目标elf格式

4.3.1典型的ELF可重定位目标文件格式

在这里插入图片描述

图4-3-1 典型的ELF可重定位目标文件

1、ELF头以16字节的序列开始,描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助两届其语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(如可执行、可重定位或者共享的)、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量。
2、.text:已编译程序的机器代码。
3、.rodata:只读数据。
4、.data:已初始化的全局变量和局部静态变量。
5、.bss:未初始化的全局变量和局部静态变量,仅是占位符,不占据任何实际磁盘空间。
6、.symtab:符号表,存放函数和全局变量(符号表)信息,不包括局部变量。
7、.rel.text:.text节的重定位信息,用于重新修改代码段的指令中的地址信息。
8、.rel.data:.data节的重定位信息,用于对被模块使用或定义的全局变量重定位的信息。
9、.debug:调试符号表,只有以-g方式调用编译器驱动程序时,才会得到这张表。
10、.line:原始C源程序中的行号和.text节中机器指令之间的映射。
11、.strtab节:字符串表,包括.symtab和.debug节中的符号表。
12、节头表:每个节的节名、偏移和大小。

4.3.2 hello.o分析

1、首先,打开终端,用readelf -S指令查看hello.o的节头表,查看节的基本信息。除了ELF头外,节头表的是ELF可重定位目标文件中最重要的部分。描述了每个节的节名文件偏移、大小、访问属性、对齐方式等。
下图中,Name列为节名,Type为类型,Offset为起始地址,Size为大小。
在这里插入图片描述
图4-3-2-1 readelf -S指令查看节头表
在这里插入图片描述
图4-3-2-2 readelf -S指令查看节头表

2、用readelf -h指令可以查看hello.o的ELF头信息。
Class:64位版本
Data:使用补码表示,且为小端法
Version:版本为1
OS/ABI:操作系统为UNIX – SYSTEM V
TYPE:REL表明这是一个可重定位文件
Machine:64位机器上编译的目标代码为Advanced Micro Devices X86-64
Entry point address:为0x0表示程序的入口地址为0
Start of program headers:为0表示没有程序头表
Start of section headers:节头表的起始位置为1240字节处
Size of section headers:64表示每个表项64个字节
Number of section headers:14表示共14个表
Section header string table index:13为.strtab在节头表中的索引
在这里插入图片描述
图4-3-2-3 readelf -h指令查看ELF头

3、readelf -s hello.o查看符号表,Name为符号名称,Value是符号相对于目标节的起始位置偏移,Size为目标大小,Type是类型,数据或函数,Bind表示是本地还是全局。
在这里插入图片描述
图4-3-2-4 readelf -s指令查看符号表

4、readelf -r hello.o发现hello.o文件中没有.rel.data段,即.data节不需要额外的重定位信息。

对于.rel.text节,Offset为需要重定位的地址与该段首地址的偏移;Info高24位为所引用的符号索引,低8位对应的重定位类型;Type有两种类型,一种是R_X86_64_PC32(R_X86_64_PLT32和R_X86_64_PC32是同一种寻址方式,详见参考文献[4])表示使用的是32位PC相对地址的引用,R_X86_64_32表示使用的是32位绝对地址的引用;Sym. Name为绑定的符号名, Addend为偏移。

一个要注意的地方是,由于PC的相对引用和动态链接的特殊,PC的值其实位下一条指令的地址,其值与我们目前想进行重定位的值有4个字节的偏移。Addend减去了4个字节,也就是说第一行指向的其实是.rodata段的首地址,第4行其实是.rodata首地址+0x26。
在这里插入图片描述
图4-3-2-5 readelf -r指令查看重定位信息

5、readelf -g hello.o显示节组信息。hello.o没有节组
在这里插入图片描述
图4-3-2-6 readelf -S指令查看节组信息

4.4 Hello.o的结果解析

在终端输入objdump -d -r hello.o查看hello.o的反汇编,结果如下:在这里插入图片描述
图4-4-1 objdump指令查看反汇编
在这里插入图片描述
图4-4-2 objdump指令查看反汇编

Hello.s文件内容如下:
在这里插入图片描述
图4-4-3 hello.s文件内容
在这里插入图片描述
图4-4-4 hello.s文件内容

通过将反汇编与hello.s比较发现,汇编指令代码几乎相同,反汇编代码除了汇编代码之外,还显示了机器代码,在左侧用16进制表示。机器指令有操作码和操作数组成,和汇编指令一一对应。最左侧为相对地址。

其中跳转指令和函数调用等指令,在反汇编代码中表示为对应地址的偏移,而在hello.s中直接表示为函数名或定义的符号。在反汇编代码中,立即数是16进制显示的,而在hello.s中立即数是以十进制显示的。

4.5 本章小结

本章首先介绍了汇编的概念和作用,接着通过实操,对hello.s文件进行汇编,生成ELF可重定位目标文件hello.o,接着使用readelf工具,通过设置不同参数,查看了hello.o的ELF头、节头表、可重定位信息和符号表等,通过分析理解可重定位目标文件的内容。最后将其与hello.s比较,分析不同,并说明机器语言与汇编语言的一一对应关系。

第5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。

5.1.2链接的作用

链接使得分离编译,一个大的应用程序可以被分解为更小、更好管理的模块,可以独立地修改和编译这些模块。

5.2 在Ubuntu下链接的命令

在终端输入
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/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello
在这里插入图片描述
图5-2-1 Ubuntu下链接ld指令

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

在这里插入图片描述
图5-3-1 可执行目标文件格式

可执行目标文件与可重定位文件稍有不同,ELF头中字段e_entry给出执行程序时的第一条指令的地址,而在可重定位文件中,此字段为0。可执行目标文件多了一个程序头表,也成为段头表,是一个结构数组。还多了一个.init节,用于定义init函数,该函数用来执行可执行目标文件开始执行时的初始化工作。因为可执行目标文件不需要重定位,所以比可重定位目标文件少了两个.rel节。

查看hello的ELF头:发现hello的ELF头中Type处显示的是EXEC,表示时可执行目标文件,这与hello.o不同。hello中的节的数量为30个。
在这里插入图片描述
图5-3-2 readelf -h查看hello的ELF头

查看hello的节头表
在这里插入图片描述
图5-3-3 readelf -S查看hello的节头表
在这里插入图片描述
图5-3-4 readelf -S查看hello的节头表
在这里插入图片描述
图5-3-5 readelf -S查看hello的节头表

发现刚才提到的30个节的具体信息,在节头表中都有显示,包括大小Size,偏移量Offset,其中Address是程序被载入虚址地址的起始地址。

查看hello的程序头表,首先显示这是一格可执行目标文件,共有12个表项,其中有4个可装入段(Type=LOAD),VirtAddr和PhysAddr分别是虚拟地址和物理地址,值相同。Align是对齐方式,这里4个可装入段都是4K字节对齐。以第一个可装入段为例,表示第0x00000~0x005bf字节,映射到虚拟地址0x400000开头的长度为0x5c0字节的区域,按照0x1000=4KB对齐,具有只读(Flags=R)权限,是只读代码段。
在这里插入图片描述
图5-3-6 readelf -l查看hello的程序头表
在这里插入图片描述
图5-3-7 readelf -l查看hello的程序头表

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
由下图,可以看到虚拟地址空间的起始地址为0x400000。
在这里插入图片描述
图5-4-1 edb查看hello的虚拟地址空间

由图5-3-4可以知道,.inerp段的起始地址为04002e0
在这里插入图片描述
图5-4-2 readelf查看.inerp段信息

可以在edb中可以找到:
在这里插入图片描述
图5-4-3 edb查看.inerp段信息

.text段的起始地址为0x4010f0,.rodata段的起始地址为0x402000,也都可以在edb中找到:
在这里插入图片描述
图5-4-4 readelf查看.text段信息
在这里插入图片描述
图5-4-5 edb查看.text段信息
在这里插入图片描述
图5-4-6 readelf查看.rodata段信息
在这里插入图片描述
图5-4-7 edb查看.rodata段信息

5.5 链接的重定位过程分析

首先,使用objdump -d -r hello对hello进行反汇编,结果如下:
在这里插入图片描述
图5-5-1 objdump查看hello反汇编结果

hello的反汇编代码多了很多节,并且发现每条数据和指令都已经确定好了虚拟地址,不再是hello.o中的偏移量。通过链接之后,也含有了库函数的代码标识信息。
接着,我们具体比较分析一下hello和hello.o的反汇编结果,下面两个图分别为hello.o和hello的反汇编的部分截图,其余同理。
在这里插入图片描述
图5-5-2 hello反汇编部分截图
在这里插入图片描述
图5-5-3 hello反汇编部分截图

图5-5-2为hello.o的反汇编代码,图5-5-3为hello的反汇编代码。可以看出,在hello.o中跳转指令和call指令后为绝对地址,而在hello中已经是重定位之后的虚拟地址。
接下来,以0x4011f6出的call指令为例,说明链接过程:
在这里插入图片描述
图5-5-4 readelf查看hello.o的重定位信息

查看该图可知,此处应该绑定第0xc个符号,同时链接器知道这里是相对寻址。接着查看hello.o的符号表,找到第12个符号puts,此处绑定puts的地址。
在这里插入图片描述
图5-5-5 readelf查看hello.o的符号表

在hello中找到puts的地址为0x401090。
在这里插入图片描述
图5-5-6 readelf查看hello.o的符号表

当前PC的值为call指令的下一条指令的地址,也就是0x4011fb。而我们要跳转到的地方为0x401090,差0x16b,因此PC需要减去0x16b,也就是加上0xff ff fe 95,由于是小端法,因此重定位目标处应该填入 95 fe ff ff。
在这里插入图片描述
图5-5-7 hello.o反汇编

5.6 hello的执行流程

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

子程序名子程序地址
hello!_start0x00000000004010f0
hello!__libc_csu_init0x0000000000401270
hello!_init0x0000000000401000
hello!frame_dummy0x00000000004011d0
hello!register_tm_clones0x0000000000401160
hello!main0x00000000004011d6
hello!printf@plt0x0000000000401040
hello!atoi@plt0x0000000000401060
hello!sleep@plt0x0000000000401080
hello!getchar@plt0x0000000000401050
hello!exit@plt0x0000000000401070
hello!__do_global_dtors_aux0x00000000004011a0
hello!deregister_tm_clones0x0000000000401130
hello!_fini0x00000000004012e8

5.7 Hello的动态链接分析

动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,将过程地址的绑定推迟到第一次调用该过程时。
首先找到.got的地址为0x403ff0。
在这里插入图片描述
图5-7-1 查看.got的地址
在edb中找到相应地址,分析在dl_init前后,这些项的变化。
在这里插入图片描述图5-7-2 dl_init前
在这里插入图片描述
图5-7-3 dl_init后

5.8 本章小结

本章首先介绍了链接的概念和作用,详细说明了可执行目标文件的结构,及重定位过程。并且以可执行目标文件hello为例,具体分析了各个段、重定位过程、虚拟地址空间、执行流程等。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念:

进程是指计算机中已运行的程序, 是系统进行资源分配和调度的基本单位, 是操作系统结构的基础。

6.1.2 进程的作用:

提供给应用程序的关键抽象:
(1)一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
(2)一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

6.2.1Shell-bash的作用:

Shell 是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。Shell 有自己的编程语言用于对命令的编辑,它允许用户编写由 shell 命令组成的程序。 Shell 编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的 Shell 程序与其他应用程序具有同样的效果。

6.2.2 Shell-bash的处理流程:

Shell 首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。 将命令行划分成小块 tokens, 程序块 tokens 被处理,检查看他们是否是 shell 中所引用到的关键字。 tokens 被确定以后, shell 根据 aliases 文件中的列表来检查命令的第一个单词。如果这个单词出现在 aliases 表中,执行替换操作并且处理过程回到第一步重新分割程序块 tokens。 对~符号和所有前面带有$符号的变量进行替换, 并将命令行中的内嵌命令表达式替换成命令。然后将命令行中的内嵌命令表达式替换成命令→将命令行中的内嵌命令表达式替换成命令→命令的检查→初始化所有的输入输出重定向→执行命令。

6.3 Hello的fork进程创建过程

进程的创建采用fork函数:pid_t fork(void);创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程fork时,子进程可以读取父进程中打开的任何文件。

父进程与创建的子进程之间最大的区别在于它们有不同的PID。子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法分辨程序是在父进程还是在子进程中。
在这里插入图片描述
图6-3-1 fork创建子进程

在这里,父进程为shell,在输入./hello的时候,首先shell会对我们输入的命令进行解析,shell会认为时执行当前目录下的可执行文件hello,因此shell会调用fork()创建一个子进程,

6.4 Hello的execve过程

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

6.5 Hello的进程执行

1、上下文信息:操作系统使用一种称为上下文切换的较高层次的异常控制流来实现多任务。其实上下文就是进程自身的虚拟地址空间,分为用户级上下文和系统及上下文。每个进程的虚拟地址空间和进程本身一一对应(因此和PID一一对应)。由于每个CPU只能同时处理一个进程,而很多时候系统中有很多进程都要去运行,因此处理器只能一段时间就要切换新的进程去运行,而实现不同进程中指令交替执行的机制称为进程的上下文切换。

2、进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
图6-5-1 上下文切换机制
图6-5-1 上下文切换机制

如上图所示,为进程A与进程B之间的相互切换。处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,为用户模式;设置模式为为内核模式。用户模式就是运行相应进程的代码段的内容,此时进程不允许运行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;而内核模式中,进程可以运行任何指令。

6.6 hello的异常与信号处理

1、异常和信号异常分为四类:中断、陷阱、故障、终止,属性如下:
在这里插入图片描述
图6-6-1 四种异常的属性
2、hello执行中可能出现的异常:
(1)中断:异步发生的。在执行hello程序的时候,由处理器外部的I/O设备的信号引起的。I/O设备通过像处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。这个异常号标识了引起中断的设备。在当前指令完成执行后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。在处理程序返回前,将控制返回给下一条指令。结果就像没有发生过中断一样。
(2)陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
(3)故障:由错误引起,可能被故障处理程序修正。在执行hello时,可能出现缺页故障。
(4)终止:不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。

3、键盘上操作导致的异常:
(1)运行时输入回车:
在这里插入图片描述图6-6-2 运行hello时输入回车
(2)运行时输入Ctrl+C
在这里插入图片描述
图6-6-3 运行hello时输入Ctrl+C
(3)运行时输入Ctrl+Z
在这里插入图片描述
图6-6-4 运行hello时输入Ctrl+Z
输入ps,监视后台程序
在这里插入图片描述
图6-6-5 输入ps

输入jobs,显示当前暂停的进程
在这里插入图片描述
图6-6-6 输入jobs
输入pstree,以树状图形式显示所有进程
在这里插入图片描述
图6-6-7 输入pstree
在这里插入图片描述
图6-6-8 输入pstree
在这里插入图片描述
图6-6-9 输入pstree
在这里插入图片描述
图6-6-10 输入pstree
输入fg,使停止的进程收到SIGCONT信号,重新在前台运行。
在这里插入图片描述
图6-6-11 输入fg
输入kill,-9表示给进程9974发送9号信号,即SIGKILL,杀死进程。
在这里插入图片描述
图6-6-12 输入kill信号,杀死hello进程

6.7本章小结

本章主要介绍了程序如何从可执行文件到进程的过程。介绍了shell的处理流程和作用。也介绍了fork函数和execve函数,及上下文切换机制等。

第7章 hello的存储管理

7.1 hello的存储器地址空间

1、逻辑地址:程序经过编译后出现在汇编代码中的地址。
2、线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式,分页机制中线性地址作为输入。
3、虚拟地址:也就是线性地址。
4、物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址

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

一个逻辑地址由两部分组成:段标识符:段内偏移量。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

索引号就是“段描述符”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。每一个段描述符由8个字节组成。全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。

Linux通过分段机制,将逻辑地址转化为线性地址。给定一个完整的逻辑地址[段选择符:段内偏移地址]。首先,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。然后,拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,基地址就知道了。最后,把基地址 + 偏移量,就是要转换的线性地址了。

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

线性地址(VA)到物理地址(PA)之间的转换通过分页机制完成。而分页机制是对虚拟地址内存空间进行分页。

正如同在cache中寻找内容也需要索引,从虚拟内存到物理内存也需要索引。因此在内存中,我们额外存储一个叫做页表的数据结构,作为对应的索引。因此,我们可以让每个进程都有一个页表,页表中的每一项都记录着该进程中对应的一页所投影到的物理地址、是否有效、还有一些其他信息等。

然而由于页的大小为212个字节,而虚拟内存有232个字节,导致页表项会有2^20项,占用空间确实太大了,而且很多页表项应该其实都是空的,毕竟进程普遍没有占用很大的地址空间。因此系统采用了多级页表的结构来进行索引。

系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

每次将虚拟地址转换为物理地址,都会查询页表来判断一个虚拟页是否缓存在DRAM的某个地方,如果不在DRAM的某个地方,通过查询页表条目可以知道虚拟页在磁盘的位置。页表将虚拟页映射到物理页。页表就是一个页表条目的数组,每一个页表条目是由一个有效位和一个n为地址字段组成。有效位表明虚拟页是否缓存在DRAM中,n位地址字段是物理页的起始地址或者虚拟页在磁盘的起始地址。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

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

页表技术虽然能让我们再给出虚拟地址的时候,很大概率通过查找页表来找到内存地址,但是查页表也是访问内存的过程,也很浪费时间。利用局部性原理,像缓存一样,将最近使用过的页表项专门缓存起来。因此出现了TLB(后备转换缓冲器,也叫快表),之后找页表项的时候,先从快表找,找不到在访问内存中的页表项。
同理,四级页表能保证页表项的数量少一些。
在这里插入图片描述图7-4-1 Core i7地址翻译的概况

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

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

7.6 hello进程fork时的内存映射

在shell中输入命令./hello后,内核调用fork函数创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的唯一的PID。为了给子进程创建虚拟内存,创建了当前进程的 mm_struct、区域结构和页表的原样副本。将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。

加载并运行 hello 需要以下几个步骤:
·删除当前进程虚拟地址中已存在的用户区域
·映射私有区域,为新程序的代码、数据、bss和栈创建新的区域结构,所有这些新的区域都是私有的、写时复制的。
·映射共享区域,将hello与libc.so动态链接,然后再映射到虚拟地址空间中的共享区域。
·设置当前进程上下文程序计数器(PC),使之指向代码区域的入口点。

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

缺页其实就是DRAM缓存未命中。当我们的指令中对取出一个虚拟地址时,若我们发现对该页的内存访问是合法的,而找对应的页表项式发现有效位为0,则说明该页并没有保存在主存中,出现了缺页故障。

此时进程暂停执行,内核会选择一个主存中的一个牺牲页面,如果该页面是其他进程或者这个进程本身页表项,则将这个页表对应的有效位改为0,同时把需要的页存入主存中的一个位置,并在该页表项储存相应的信息,将有效位置为1。然后进程重新执行这条语句,此时MMU就可以正常翻译这个虚拟地址了。

7.9动态存储分配管理

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

分配器有两种基本风格:
(1)显式分配器:要求应用显式地释放任何分配的块,例如C标准库提供的malloc程序包。
(2)隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就是放这个块,也被称为垃圾收集器。
分配器简单来说有以下几种实现方式:
在这里插入图片描述图7-9-1 分配器的实现方式

7.10本章小结

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

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m字节的序列:B0 , B1 , … , Bk , … , Bm-1
所有的 IO 设备(例如网络、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行:

设备的模型化:文件
设备管理:unix io接口。

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O 接口

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

8.2.2函数

(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的当前文件位置。
(5) lseek函数:off_t lseek(int fd, off_t offset, int whence);
应用程序显示地修改当前文件的位置。
(6) stat函数:int stat(const char *filename,struct stat *buf);
以文件名作为输入,并填入一个stat数据结构的各个成员。

8.3 printf的实现分析

printf函数的函数体:
在这里插入图片描述
图8-3-1 printf函数的函数体

在红框中,第一行目的是让argv指向第一个字符串;第二句的作用是格式化,并返回要打印的字符串的长度,第三句的作用是调用write函数将buf的前i个字符输出到终端,调用了unix I/O。

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

8.4 getchar的实现分析

getchar的源代码如下:
在这里插入图片描述
图8-4-1 getchar源代码

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

8.5本章小结

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

结论

一开始,hello.c被程序员一字一键地敲进电脑,轻松地点击运行后,hello world!就出现在了屏幕上。虽然看起来简单,但是在深入了解后才发现,这一切并没有那么简单。

最开始,hello.c程序安静地呆在磁盘里,等待着被执行。终于,hello.c经过了预处理,头文件被引入、宏被展开等,变成了hello.i文件。接着经过编译器的处理,变成了hello.s文件后,又经过汇编器的转换,变成了只有机器认识的二进制代码文件hello.o。但是这是还没有结束,hello.o文件还需经过链接才能最终成为可执行文件hello。

可执行文件成功生成后,为了执行它,程序员在终端输入./hello。虽然结果瞬间就出来了,但是中间还是经历了很多。

shell解析命令行输入的命令,然后调用fork创建子进程,并用execve映射到虚拟内存中。当CPU执行到hello时,开始读取对应的虚拟内存地址,通过缺页异常将hello放入主存中。之后通过四级页表、一层层缓存……终于hello被加载到了处理器内部。

然后,再通过I/O包装的I/O函数,终于结果被输出到终端。

最后hello.c程序被回收,重新进入硬盘……

虽然hello的一生如此短暂,但是却坎坷而精彩!

计算机系统这门课整体来说,感觉知识很多,难度也不小,不过很有趣。计算机系统远比我想象中的更复杂得多得多,其中的奥秘也很多。通过学习这门课,通过实际做实验、上网查资料等,我也了解到了许多新知识,提高了学习能力。

总之,计算机系统这门课让我受益颇多,我之后也将继续学习相关知识。

附件

hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o_elf:hello.o用readelf -a hello.o指令生成的文件。
hello_elf:hello用readelf -a hello指令生成的文件。

参考文献

[1] 袁春风. 计算机系统基础. 北京:机械工业出版社,2018.7(2019.8重印)
[2] Randal E. Bryant;David R. O’Hallaron. 深入理解计算机系统. 北京:机械工业出版社,2016.7(2019.3重印)
[3] 局部变量是存放在栈中,还是存放在堆栈中?_百度知道 (baidu.com)
[4] sourceware.org Git - binutils-gdb.git/commitdiff
[5] 一个简单程序从编译、链接、装载(执行)的过程-静态链接 - 知乎 (zhihu.com)
[6] 编译和链接的过程_douguailove的博客-CSDN博客_编译过程
[7] (1条消息) readelf命令使用说明_木虫下的博客-CSDN博客_readelf
[8] 【转】linux汇编.section .text .data 与.global - 比较懒 - 博客园 (cnblogs.com)

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 全国计算机等级考试二级教程——java语言程序设计的PDF版本是一本介绍Java编程语言的教程书籍。这本书对于计算机等级考试二级的学习者来说非常有用。 Java语言是一种通用的高级编程语言,它被广泛应用于跨平台应用程序的开发。这本教程主要围绕Java语言的程序设计展开,内容包括Java语言的基础知识、面向对象编程、异常处理、文件IO操作、多线程编程、网络编程等。 这本教程通过分章节详细介绍了Java语言的各个方面,从基础知识开始,逐渐深入,适合初学者学习。每个章节都通过理论讲解和实践代码演示相结合的方式,让读者能更好地理解和运用所学知识。 此外,这本教程还提供了丰富的练习题目和编程实践,帮助读者巩固所学内容,并提升编程能力。同时,书中还包含了考试相关的重点知识点和实例,能够帮助读者更好地应对全国计算机等级考试二级的相关题目。 总体来说,全国计算机等级考试二级教程——java语言程序设计PDF是一本很好的学习工具,适合计算机等级考试二级的学习者使用。无论是想了解Java编程语言的基础知识,还是为计算机等级考试二级做准备,这本教程都能够提供相应的帮助。 ### 回答2: 《全国计算机等级考试二级教程——Java语言程序设计PDF》是一本关于Java语言程序设计的考试教材。该书内容分为几个主要部分: 首先,该书从Java语言的基础知识入手,介绍了Java的背景和特点,以及Java的安装与配置。 其次,该书详细讲解了Java程序的编写与运行。从编写一个简单的Hello World程序开始,逐步引导读者学习Java的语法规则、基本数据类型、运算符以及控制结构等。 然后,该书介绍了一些常用的Java类库和API,如String类、Math类、Scanner类等,以及Java的面向对象编程思想和相关的概念,如类、对象、继承、封装、多态等。同时,还涉及到实例变量、类变量、方法的定义和使用等内容。 最后,该书还对异常处理、文件操作、线程编程等Java高级特性进行了简要介绍,使读者能够初步了解这些内容。 此外,该书还通过提供大量的例题和练习题,帮助读者巩固所学知识,培养解决实际问题的能力。 总结来说,这本教材是辅助学习者准备全国计算机二级考试的理想参考书籍。它详细介绍了Java语言的基础知识和应用,适合初学者入门。读者通过学习这本教材,可以系统地掌握Java语言的基本语法、面向对象编程以及高级特性,为考试做好充分准备。 ### 回答3: 全国计算机等级考试二级教程——Java语言程序设计 pdf 是一本关于Java编程语言的教材。Java作为一种高级编程语言,在计算机行业中应用广泛,掌握Java编程可以使开发人员更好地进行软件开发和系统设计。 这本教材主要分为几个部分。首先,它介绍了Java编程语言的基本概念和语法规则,例如变量、数据类型、运算符、流程控制等。通过学习这些内容,读者可以了解Java的基本特点和编程规范。 其次,教材详细介绍了Java的面向对象编程(OOP)思想和相关的知识。面向对象编程是现代软件开发中的一种重要方法,它通过封装、继承和多态等机制,使得程序更易读、易维护、易扩展。读者将学习如何定义类、创建对象、定义方法和属性等。 此外,教材还介绍了Java中常用的类库和API,包括输入输出流、字符串处理、异常处理、图形界面等。这些类库和API提供了大量的功能和工具,可以帮助开发人员更加高效地编写程序。 在教材的最后,还有一些实例和练习题供读者巩固所学知识。通过实际的编程实例和练习,读者可以提高自己的编程能力和解决问题的能力。 总之,全国计算机等级考试二级教程——Java语言程序设计 pdf 是一本很好的学习资料,对于想要学习Java编程的人来说,是一本不可或缺的参考书。通过学习这本教材,读者可以系统地了解和掌握Java编程的基础知识和技能,为将来的职业发展打下良好的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值