计算机系统大作业 程序人生-Hello’s P2P

摘 要

本文围绕Hello程序,介绍了其在Linux系统下的生命周期,分析了从hello.c源代码文件经过预处理、编译、汇编、链接、加载、运行以至终止的全过程,全面介绍了Hello的一生,并结合课程所学知识说明了其中所涉及的概念和知识,梳理、回顾了课程所学的主要知识点,基本做到了把计算机系统整个的体系串联在一起,融会贯通。

关键词:Hello;进程;操作系统;计算机系统;Shell

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

第1章 概述

1.1 Hello简介

1.1.1 P2P

Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。在Linux系统中,源代码文件hello.c先经cpp预处理生成文本文件hello.i,接着hello.i经cc1编译器编译生成汇编文件hello.s,hello.s经ld链接器链接生成可执行程序文件hello。最后在shell中键入命令(./hello)后,shell通过fork创建子进程,hello 便从可执行程序(Program)变成了进程(Process)。

1.1.2 020

Hello的020是指“From 0 to 0”。Hello程序执行前,不占用任何内存空间。P2P过程后,shell调用execve函数,系统会将hello文件载入内存,执行相关代码,程序结束以后,shell回收此子进程,内存中所有与之相关的状态信息和数据结构被清除。这就是Hello的020。

1.2 环境与工具

  1. 硬件环境
    Intel® Core™ i7-10510U CPU @ 1.80GHz 2.30 GHzRAM: 16.00GB
    RAM:16.0 GB
    SSD: 512GB
  2. 软件环境
    Windows10 64位
    Vmware station Pro
    Ubuntu 20.04.1
  3. 开发与调试工具
    Visual Studio
    CodeBlocks
    Notepad ++
    cpp(预处理器)
    gcc(编译器)
    as(汇编器)
    ld(链接器)
    GNU readelf
    GNU gdb
    EDB等

1.3 中间结果

计算机系统大作业 程序人生-Hello’s P2P_第1张图片

1.4 本章小结

本章介绍了Hello程序的P2P、O2O的概念,接着介绍了我在大作业完成过程中所用到的环境及开发工具,最后展示了我所有用到的中间文件。
(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

预处理指的是程序在编译之前进行的处理,是计算机在处理一个程序时所进行的第一步处理,可以进行代码文本的替换工作,但是不做语法检查。在编译代码之前,编译工具会自动调用预处理程序来执行这些预处理命令,当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理命令部分做处理,处理完毕自动进入对源程序的编译。它根据以字符#开头的命令,修改原始的C程序,通常得到以.i作为文件扩展名的C程序。下图是hello.c文件里预处理的命令:
计算机系统大作业 程序人生-Hello’s P2P_第2张图片
图2.1 hello.c预处理的命令

2.1.2 预处理的作用

预处理的作用是修改原始的C程序,得到一个可读但不包含任何宏定义的文件。C语言的预处理主要包括: 宏定义、文件包含、条件编译等。
宏定义在预处理的过程中会进行宏替换。在具体语句中表现为#define。宏定义分为两种,无参数的宏定义要用实际值替换用#define定义的字符或字符串;带参数的宏定义,可以为宏设置参数。对带参数的宏,在调用中不仅要宏展开,还要用实参去代换形参。特别地,在宏替换中,只进行替换。
文件包含指的是对代码中出现的#include语句进行处理。#include能够告诉预处理器读取源程序中所引用的系统的源文件,并且将这一段代码直接插入到程序文件中,最终保存为.i文件中。在hello.c文件中,预处理会对#include
#include #include 三条语句进行文件包含的处理。
条件编译的功能是根据条件有选择性地保留或者放弃源文件中的某些内容,它们以#if、#elif、#ifdef或者#ifndef指令开始,结束于#endif,中间可以有#else和#elif指令。使用条件编译可以使目标程序变小,在满足条件之后才会进行编译。使用条件编译,可方便地处理程序的调试版本和正式版本,也可使用条件编译使程序的移植更方便。

2.2在Ubuntu下预处理的命令

在Linux下对hello.c的预处理指令为:cpp hello.c > hello.i
在这里插入图片描述
图2.2 预处理命令

2.3 Hello的预处理结果解析

观察hello.c和hello.i文件大小,发现hello.i文件增大了很多字节(由于插入相关头文件内容)

计算机系统大作业 程序人生-Hello’s P2P_第3张图片
图2.3 hello.i文件
由于插入头文件 stdio.h unistd.h stdlib.h,hello.i文件被扩展到了3060行。cpp 到 Ubuntu 中默认的位置找到并打开标准库(如stdio.h)文件,标准库文件里用到了#define,因此cpp 对其中的define 宏定义递归展开,最终的.i文件中不会出现#define;此外标准库文件里还使用了条件编译的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。
计算机系统大作业 程序人生-Hello’s P2P_第4张图片
图2.4 hello.i的部分库文件
计算机系统大作业 程序人生-Hello’s P2P_第5张图片
图2.5 hello.i中原.c代码

2.4 本章小结

本章主要介绍了预处理的概念和功能(如宏定义、文件包含、条件编译),同时具体实现理hello.c文件的预处理和对hello.i的文本文件解析,展示了Linux下预处理的cpp指令的具体操作,详细阐释了预处理的内涵。
(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译是对.c源文件经预处理输出生成的预处理文件.i进行编译操作生成汇编代码文件的过程。编译程序通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的汇编代码。对于hello.c文件,编译器将经过预处理的文件 hello.i 翻译成文件 hello.s。

3.1.2 编译的作用

编译能够检查代码的规范性,会对代码进行如下操作:
① 词法分析:对由字符组成的单词从左至右逐个字符地进行扫描,产生一个
个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
② 语法分析:编译程序的语法分析器以词法分析产生的单词符号作为输入,
分析单词符号串是否形成符合语法规则的语法单位,最后看是否构成一个符合要求的程序。
③ 中间代码:中间代码的作用是可使编译程序的结构在逻辑上更为简单明
确。
④ 代码优化:对程序进行多种等价变换(不改变程序的运行结果),便于生
成更有效(目标代码运行时间较短、占用的存储空间较小)的目标代码。
⑤ 目标代码:目标代码生成器把语法分析后或优化后的中间代码变换成汇编
代码。
如果检查没有错误则生成汇编语言;否则产生错误报告。

3.2 在Ubuntu下编译的命令

根据大作业PPT相关说明,编译的命令为:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

在这里插入图片描述
图3.1 编译的命令
计算机系统大作业 程序人生-Hello’s P2P_第6张图片
图3.2 hello.s文件

3.3 Hello的编译结果解析

3.3.1 数据类型

3.3.1.1 常量

1.字符串:程序中保存了两个字符串常量,被保存在.rodata中。
计算机系统大作业 程序人生-Hello’s P2P_第7张图片
图3.3 源程序代码
在这里插入图片描述
图3.4 汇编代码
2.常数:循环判定条件值及循环终止值被保存在.text 中。
计算机系统大作业 程序人生-Hello’s P2P_第8张图片
图3.5 源程序代码中常数位置
计算机系统大作业 程序人生-Hello’s P2P_第9张图片
图3.6 汇编代码中常数位置

3.3.1.2 变量

1.全局变量:
初始化的全局变量储存在.data 节,它的初始化不需要汇编语句,而是直接完成的。
计算机系统大作业 程序人生-Hello’s P2P_第10张图片
图3.7 汇编代码中全局变量
2. 局部变量:
局部变量存储在寄存器或栈中。本程序局部变量 i (int 型)在运行时保存在栈中(R[%rbp]-4的内存位置),在汇编代码中,对i进行赋值(movl指令)、自加(addl指令)、和循环终止条件比较(cmpl指令)。
在这里插入图片描述
图3.8 源代码局部变量位置及操作
在这里插入图片描述
图3.9 汇编代码中局部变量

3.3.2 算术操作

在for循环中,对循环变量(局部变量)i使用了赋值运算(i=0)自加运算(i++),汇编代码中分别为movl指令和addl指令。特别地,局部变量i在栈中,采用间接引用寻址。
在这里插入图片描述
图3.8 源代码算术操作
计算机系统大作业 程序人生-Hello’s P2P_第11张图片
图3.9 汇编代码中算术操作

3.3.3 关系操作与控制转移

  1. if 条件判断的关系操作与控制转移:
    cmpl指令设置条件码(ZF),若 ZF = 1,即argc 等于4,条件不成立,控制转移至.L2(for 循环部分);若 ZF = 0,即argc 不等于 4,则输出提示信息并非正常退出。
    在这里插入图片描述
    图3.10 源代码中if判断语句
    在这里插入图片描述
    图3.11 汇编代码中if判断语句
  2. for 循环的关系操作与控制转移:
    cmpl指令设置条件码,当(SF^OF) | ZF = 0时,继续进行循环;(SF^OF) | ZF =1时,即i的值大于等于7,循环终止,控制转移至.L4,跳出循环体执行接下来的语句。特别地,源程序中的i<8在汇编代码中会自动替换成与之等价的i<=7

在这里插入图片描述
图3.12 源代码中for语句
在这里插入图片描述
图3.13 汇编代码中的for语句

3.3.4 数组/指针/结构操作

主函数 main()的第二个参数是 char *argv[],在argv 数组中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串。通过%rsi-8 和%rax-16,分别得到 argv[1]和 argv[2]两个字符串。
计算机系统大作业 程序人生-Hello’s P2P_第12张图片
图3.14 汇编代码中的argv数组

3.3.5 函数操作

X86-64机器的函数调用方式是1~6个参数用寄存器存储,剩下的参数保存在栈中

  1. main 函数: :
    传递参数的参数是int argc, char *argv[],分别用寄存器%edi和%rsi存储。通过系统启动函数调用来实现main函数调用。最后设置%eax为0并且返回,即return 0。
    在这里插入图片描述
    图3.15 源代码main函数声明
    计算机系统大作业 程序人生-Hello’s P2P_第13张图片
    图3.16 汇编代码main函数声明
    下图汇编指令展示了启动函数调用为汇编器进行相关处理提供的信息,标记了程序入口等信息:
    计算机系统大作业 程序人生-Hello’s P2P_第14张图片
    图3.17 汇编代码程序入口
  2. printf 函数:
    第一个调用call puts参数是字符串参数首地址,当if的判定条件满足后调用;第二个调用call printf 时传入了argv[1]和argv[2]的地址,在for循环中调用。

计算机系统大作业 程序人生-Hello’s P2P_第15张图片
图3.18 源代码的printf函数

计算机系统大作业 程序人生-Hello’s P2P_第16张图片
计算机系统大作业 程序人生-Hello’s P2P_第17张图片
图3.19 汇编代码的printf函数调用

  1. exit 函数:
    传入退出状态值,再执行退出命令。在if 判断条件满足后,就可以调用exit函数。该程序使用寄存器%rdi传递参数值(1),调用exit函数退出,不需要函数返回。
    在这里插入图片描述
    图3.20 源代码exit函数
    在这里插入图片描述
    图3.21 汇编代码调用exit函数
  2. sleep函数:
    传递的参数是休眠时间(通过atoi函数转换而来),在for循环下被调用8次,函数的返回值被忽略。

在这里插入图片描述
图3.22 源代码的sleep函数
计算机系统大作业 程序人生-Hello’s P2P_第18张图片
图3.23 汇编代码的sleep函数

  1. getchar 函数:
    main函数通过call指令调用getchar函数,不需要传递参数。
    在这里插入图片描述
    图3.24 源代码的getchar函数
    计算机系统大作业 程序人生-Hello’s P2P_第19张图片
    图3.25 汇编代码的getchar函数调用

3.4 本章小结

本章主要介绍了编译的概念以及过程。同时利用汇编命令对hello.c产生汇编代码hello.s,另外,针对汇编代码文件详细介绍了汇编代码如何实现变量、常量、算术操作、关系操作、函数操作等。这些工作让我更深刻地理解了 C 语言的数据与操作,对汇编语言有了更好的掌握。
本章内容的完成过程加深了我对编译阶段的理解,同时也促进了我对第三章相关知识的复习。
(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1 汇编的概念

驱动程序运行汇编器 as,将汇编语言(.s文件)翻译成机器语言指令,并将这些指令打包成可重定位目标文件(.o文件),这一过程称为汇编,.o文件是二进制编码文件,包含程序的机器指令编码。

4.1.2 汇编的作用

汇编的作用就是将高级语言转化为机器可直接识别执行的代码文件的过程。汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 二进制目标文件中。
4.2 在Ubuntu下汇编的命令
汇编的命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
在这里插入图片描述
图4.1 汇编的命令
计算机系统大作业 程序人生-Hello’s P2P_第20张图片
图4.2 输入汇编指令后的hello.o文件

4.3 可重定位目标elf格式

4.3.1 readelf命令

输入命令:readelf -a hello.o > ./elf.txt
将 hello.o 中 ELF 格式相关信息重定向至文件 elf.txt
在这里插入图片描述
图4.3 readelf命令

4.3.2 ELF头

ELF头包含了操作系统、大/小端序,系统架构,ELF 头大小,节的大小和数量等信息。其中的Magic用于标识ELF文件,7f 45 4c 46分别对应ASCII码的Del、字母E、字母L、字母F,操作系统会对这些字符进行验证。
计算机系统大作业 程序人生-Hello’s P2P_第21张图片
图4.4 ELF头

4.3.3 节头目表

节头目表描述了.o 文件中的14个节的名称、类型、地址、偏移量、所占空间大小等信息。其中每个节都是从0开始,以便进行重定位操作。.text段可执行的,但是不能写;.bss段大小为0;.data段和.rodata段都不可执行,但.rodata段不可写。
下面是文件的头节目表:
计算机系统大作业 程序人生-Hello’s P2P_第22张图片
图4.5 节头目表

4.3.4 重定位节

重定位节介绍了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断如何计算地址值并使用偏移量等信息计算出正确的地址。
本程序需要重定位的符号有:.rodata, puts, exit, printf, aoti, sleep, getchar。重定位类型仅有R_X86_64_32和R_X86_64_PLT32两种。
计算机系统大作业 程序人生-Hello’s P2P_第23张图片
图4.6 重定位节

4.3.5 符号表

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

计算机系统大作业 程序人生-Hello’s P2P_第24张图片
图4.7 符号表

4.4 Hello.o的结果解析

使用命令:objdump -d -r hello.o > hello.asm
在这里插入图片描述
图4.8 objdump指令
在这里插入图片描述
图4.9 hello.asm文件

比较hello.s和hello.asm文件可以发现它们之间的差异:

  1. 分支转移:
    分支转移:跳转语句之后,hello.s 中是.L2 和.LC1 等代码块的名称,而反汇
    编代码中跳转指令之后是相对于main函数起始位置偏移的地址。
    计算机系统大作业 程序人生-Hello’s P2P_第25张图片
    图4.10 差异1:hello.asm文件
    计算机系统大作业 程序人生-Hello’s P2P_第26张图片
    图4.11 差异1:hello.s文件
  2. 函数调用:
    在 hello.s 文件中,call 之后直接跟着函数名称,而在反汇编得到的 hello.asm
    中,call 的目标地址是当前指令的下一条指令。因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址。
    因此在.rela.text 节中为其添加了重定位条目。

计算机系统大作业 程序人生-Hello’s P2P_第27张图片
图4.12 差异2:hello.asm文件
计算机系统大作业 程序人生-Hello’s P2P_第28张图片
图4.13 差异2:hello.s文件
3. 其他差异:
hello.s 中的操作数是十进制,hello.asm 反汇编代码中的操作数是十六进制;hello.s 中提供给汇编器的辅助信息在反汇编代码中不再出现。
计算机系统大作业 程序人生-Hello’s P2P_第29张图片
图4.14 差异3:hello.asm文件
计算机系统大作业 程序人生-Hello’s P2P_第30张图片
图4.15 差异3:hello.s文件

4.5 本章小结

本章主要介绍了汇编的概念、作用、可重定向目标文件的结构及对应反汇编代码等。经过汇编阶段,汇编语言代码转化为机器语言,生成的可重定位目标文件。通过对比 hello.s 和hello.o反汇编代码hello.asm文件的异同之处,我更加理解了汇编语言到机器语言的转化。完成本章内容的过程加深了我对汇编过程、ELF 格式以及重定位的理解。
(第4章1分)

第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/7/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello
在这里插入图片描述
图5.1 链接命令
计算机系统大作业 程序人生-Hello’s P2P_第31张图片
图5.2 hello文件

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

命令:readelf -a hello > hello.elf

  1. ELF头:hello.elf 的 ELF 头与上文中的elf.txt文件中的 ELF 头包含的信息种类基本相同。
    计算机系统大作业 程序人生-Hello’s P2P_第32张图片
    图5.3 ELF头

  2. 节头:各段的基本信息(起始地址、大小、偏移量等其他属性)记录在节头部表中
    计算机系统大作业 程序人生-Hello’s P2P_第33张图片
    图5.4 节头

  3. 程序头:程序头是一个结构数组,描述了系统准备程序执行所需的段或其
    他信息。
    计算机系统大作业 程序人生-Hello’s P2P_第34张图片
    图5.5 程序头

5.4 hello的虚拟地址空间

输入命令:edb --run hello,用edb加载hello,界面如图所示:
计算机系统大作业 程序人生-Hello’s P2P_第35张图片
图5.6 edb界面

通过点击Plugins菜单中的SymbolsViewer选项弹出Symbols窗口以查看进程中各个符号的信息。从图中可以看到,程序入口点_start的地址和ELF头中所显示的0x401090相同,init的位置和ELF头中所显示的0x401000相同,其他符号均相同。
计算机系统大作业 程序人生-Hello’s P2P_第36张图片
图5.7 ELF图中信息
计算机系统大作业 程序人生-Hello’s P2P_第37张图片
图5.8 Data Dump和Symbols图示
在上图中,我们分别可以看到程序printf函数的字符串等内容,明了各段的虚拟地址与节头部表的对应关系。

5.5 链接的重定位过程分析

命令:objdump -d -r hello > hello2.asm
在这里插入图片描述
图5.9 生产反汇编语句

  1. 链接后函数数量增加。hello2.asm 中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt 等函数的代码。
    计算机系统大作业 程序人生-Hello’s P2P_第38张图片
    图5.10 对比1

  2. 函数调用指令 call 的参数发生变化。在链接过程中,链接器解析了重定位
    条目,call 之后的字节代码被链接器直接修改为目标地址与下一条指令的
    地址之差,指向相应的代码段,从而得到完整的反汇编代码。
    计算机系统大作业 程序人生-Hello’s P2P_第39张图片
    图5.11 对比2

  3. hello.asm比 hello.o 的反汇编代码多了一些节,如下图中.init, .plt, .sec等。
    计算机系统大作业 程序人生-Hello’s P2P_第40张图片
    图5.12 对比3

  4. hello 中没有hello.o中的重定位条目,并且跳转和函数调用的地址在 hello 中都变成了虚拟内存地址。在链接过程中,链接器解析了重定位条目。在hello.o 的反汇编代码中,函数在链接之后才能确定运行执行的地址,故在.rel.text 节中为其添加了重定位条目。
    计算机系统大作业 程序人生-Hello’s P2P_第41张图片
    图5.13 对比3

5.6 hello的执行流程

在edb中执行hello,并添加程序参数(即在终端中输入./hello 120L021130 王宇博),结果如下图:
计算机系统大作业 程序人生-Hello’s P2P_第42张图片
图5.14 edb

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

计算机系统大作业 程序人生-Hello’s P2P_第43张图片

5.7 Hello的动态链接分析

编译器无法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。延迟绑定通过全局偏移量表(GOT)和过程链接表(PLT)的协同工作实现函数的动态链接,其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。

.got与.plt节保存着全局偏移量表GOT,在elf文件中查看,其内容从地址0x404000开始。
在这里插入图片描述
图5.15 elf文件
下图为调用dl_init之前.got.plt段的内容:
计算机系统大作业 程序人生-Hello’s P2P_第44张图片
图5.16 调用dl_init之前.got.plt段的内容
调用dl_init之后.got.plt段的内容为:
计算机系统大作业 程序人生-Hello’s P2P_第45张图片
图5.17 调用dl_init之后.got.plt段的内容

5.8 本章小结
本章主要介绍了由可重定位目标文件hello.o链接生成可执行目标文件hello的过程,详细介绍了链接的概念、作用及工作,并验证了hello的虚拟地址空间与节头部表信息的对应关系,分析了hello的执行流程。最后对hello程序进行了动态链接分析。加深了我对链接和重定位的相关概念知识的理解。
(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念

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

6.1.2 进程的作用

运行程序时,shell会创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。
进程提供给应用程序的两个关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。

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

6.2.1 Shell-bash的作用

Shell-bash 是一个交互型应用级程序,提供了用户与内核进行交互操作的一种接口。它能够接收用户输入的内置或外置命令并把它送入内核去执行。

6.2.2 Shell-bash的处理流程

  1. shell 首先通过对输入的命令行进行分词或的所有参数,并检查命令是否是内置命令。
  2. 若是内置命令则执行该命令。
  3. 若不是再检查是否是一个应用程序。shell 在搜索路径里寻找这些应用程序(PATH)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给内核。
  4. shell 能够随时接受键盘输入信号,并对这些信号进行相应处理(停止或者终止等指令)。
    6.3 Hello的fork进程创建过程
    在Shell里输入命令./hello 120L021130 王宇博 1,执行生成的可执行文件hello:
    计算机系统大作业 程序人生-Hello’s P2P_第46张图片
    图6.1 带参数运行可执行文件
    fork 进程的创建过程为:
    (1)输入命令执行 hello 后,带参执行当前目录下的可执行文件 hello,父进程会判断不是内部指令,会通过 fork 函数创建子进程。
    (2)子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的 PID,子进程可以读取父进程打开的任何文件。
    (3)fork 函数只会被调用一次,但会返回两次,在父进程中,fork 返回子进程的 PID,在子进程中,fork 返回 0。
    (4)当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由 init 进程回收子进程。
    6.4 Hello的execve过程
    fork函数创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行可执行目标文件hello,且带列表argv和环境变量列表envp,在不出现异常的情况下(如找不到hello文件)execve 函数不会返回。它调用启动代码,并将控制传递给新程序的主函数main。
    execve过程删除已存在的用户区域并将其初始化,然后通过跳转到程序的第一条指令来运行该程序。将私有的区域映射进来,然后共享区映射进来。设置当前进程的上下文中的程序计数器PC,使之指向代码区域的入口点,这样就完成了在子进程中的加载。

6.5 Hello的进程执行

6.5.1 概念

  1. 上下文:内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
  2. 进程时间片:一个进程执行它的控制流的一部分的每一个时间段叫做时间片(time slice),多任务也叫时间分片(time slicing)。
  3. 进程调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这种决策称为调度,是由内核中的调度器代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
  4. 用户态与核心态的转换:为了保证系统安全,需要限制应用程序所能访问的地址空间范围。因而存在用户态与核心态的划分,内核态拥有最高的访问权限,而用户态的访问权限会受到一些限制。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,一定程度上保证了系统的安全性。

6.5.2 Hello进程分析

在hello运行以后,shell为hello创建了一个子进程,这个子进程与shell有独立的逻辑控制流。当hello调用sleep函数时,sleep函数会向内核发送请求将hello挂起,以实现处理器资源的最大化利用。同时,将 hello 进程从运行队列加入等待队列,计时结束以后,sleep函数返回,触发一个中断,使得内核将hello进程从等待队列中移出,并内核模式转为用户模式, hello进程就可以继续执行其逻辑控制流。
计算机系统大作业 程序人生-Hello’s P2P_第47张图片
图6.1 进程上下文切换

6.6 hello的异常与信号处理

6.6.1 hello的异常与处理方式

异常的类型:
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不会返回
处理方式:
计算机系统大作业 程序人生-Hello’s P2P_第48张图片
图6.2 四种处理方式
hello的信号:
SIGINT:键入Ctrl-C后内核向hello进程发送,终止程序。
SIGSTP:键入Ctrl-Z后内核向hello进程发送,停止直到下一个SIGCONT。
SIGCONT:键入fg后内核向hello进程发送,若停止则继续执行。
SIGKILL:键入kill -9 后内核向hello进程发送,终止程序。

6.6.2 hello运行中按键盘的不同结果

  1. 程序正常运行时会打印8条提示信息,最终任意输入一个字符(或回车)结束,并回收进程。
    计算机系统大作业 程序人生-Hello’s P2P_第49张图片
    图6.3 正常情况

  2. 在程序运行时多次按回车:会多打印几个空行程序能够正常结束。
    计算机系统大作业 程序人生-Hello’s P2P_第50张图片
    图6.4 多按空格

  3. 按Ctrl + Z:Shell进程收到SIGSTP信号,Shell输出提示信息并挂起hello进程。
    计算机系统大作业 程序人生-Hello’s P2P_第51张图片
    图6.5 按Ctrl + Z

接着使用ps和jobs查看hello进程,发现他被挂起而非被回收,job代号为1
计算机系统大作业 程序人生-Hello’s P2P_第52张图片
图6.6 ps和jobs命令

接着在shell中输入pstree命令,将所有进程以树状图显示(仅展示了与hello进程相关的部分):
计算机系统大作业 程序人生-Hello’s P2P_第53张图片
图6.7 pstree命令

接着输入fg命令,将hello进程再次调到前台执行,shell会先打印运行hello的命令,再从上次挂起的位置继续打印,最终正常完成进程回收:
计算机系统大作业 程序人生-Hello’s P2P_第54张图片
图6.8 fg命令

最后输入kill命令,来杀死指定进程:
计算机系统大作业 程序人生-Hello’s P2P_第55张图片
图6.9 kill命令
4. 按下Ctrl + C,shell进程收到SIGINT信号,shell结束并回收hello进程。
计算机系统大作业 程序人生-Hello’s P2P_第56张图片
图6.10 Ctrl-C命令
5. 不停乱按
在程序执行过程中乱按所造成的输入均存入缓冲区,当getchar的时候读会读入,进程结束后,缓冲区剩余的字符串会当作命令行输入。
计算机系统大作业 程序人生-Hello’s P2P_第57张图片
图6.11 不停乱按

6.7本章小结

本章介绍了进程和shell-bash的概念及作用,同时又专门针对hello进程来研究他的创建、加载、终止及其对应的fork,execve函数,以及不同键盘输入导致的运行结果,分析了它再执行过程中的异常、信号及其处理。本章内容的完成,让我对课程第8章——异常控制流的内容有了更深刻的理解,对进程、异常、信号等概念有了更深刻的认识。

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址
    逻辑地址是指由程序hello产生的与段相关的偏移地址部分,如:hello.o中代码与数据的相对偏移地址。
  2. 线性地址
    逻辑地址经过段机制转化后为线性地址,是逻辑地址到物理地址的中间阶段,它是处理器可寻址空间的地址。程序hello代码会产生逻辑地址,或者说是段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
  3. 虚拟地址
    现代处理器使用一种称为虚拟寻址的寻址形式。使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存。虚拟地址的集合即为线性地址空间
  4. 物理地址
    物理地址是指CPU通过地址总线的寻址,找到真实的物理内存对应的地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就是物理地址。

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

Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,从而方便进行段式管理。
在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符包含三部分:
索引号:确定当前使用的段描述符在描述符表中的位置。
TI:根据TI的值判断选择全局描述符表(GDT)或选择局部描述符表(LDT)。
RPL:根据RPL判断重要等级。RPL=00,为第0级,位于最高级的内核;RPL=11,为第3级,位于最低级的用户状态。
通过索引就可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址、虚拟地址。

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

线性地址到物理地址之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。VM系统会将虚拟内存分割为大小固定的块——虚拟页,每个虚拟页的大小P=2P,物理内存被分割成物理页,大小也为P字节。通过段式管理过程,我们可以得到了线性地址或者虚拟地址(VA)。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取。
计算机系统大作业 程序人生-Hello’s P2P_第58张图片
图7.1 Hello的页式管理
若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。

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

TLB指的是:每次CPU产生一个虚拟地址时,MMU(内存管理单元)通过查询一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降到1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
多级页面指的是:多级页表为层次结构,用于压缩页表。这种方法从两个方面减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在;第二,只有一级页表才需要总是在主存中,虚拟内存系统可以在需要时创建、页面调出或调入二级页表,最经常使用的二级页表才缓存在主存中,减少了主存的压力。
虚拟地址到物理地址的变换过程指的是:对于四级页表,CPU产生虚拟地址VA,它被划分为4个VPN和1个VPO,将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN与VPO组合成物理地址PA。对于前3级页表,每级页表中的每个PTE都指向下一级某个页表的基址。最后一级页表中的每个PTE包含某个物理页面的PPN。VPN1确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。若查询PTE的时候发现不在物理内存中,则引发缺页故障。
计算机系统大作业 程序人生-Hello’s P2P_第59张图片
图7.2 Core i7页表翻译

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

CPU发送一条虚拟地址,MMU按上一节描述的操作获得了物理地址,并根据物理地址、L1高速缓存的组数和块大小确定高速缓存块偏移(CO)、组索引(CI)和高速缓存标记(CT)。
使用CI进行组索引寻找正确的组,对组中每行的标记与CT进行匹配。如果匹配成功且块的valid标志位为1,则命中,然后根据CO取出数据并返回数据给CPU。若未找到相匹配的行或者valid标志位为0,则L1未命中,继续在下一级高速缓存(L2)中进行类似过程的查找。若仍未命中,还要在L3高速缓存中进行查找。三级Cache均未命中则需要访问主存获取数据。
若有一级高速缓存未命中,则需在得到数据后更新未命中的Cache。先判断其中是否有空闲块,若有空闲块(有效位为0),则直接将数据写入;若不存在,则需根据替换策略(如LRU、LFU策略等)驱逐一个块再写入。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

在shell中的进程中执行了如下的execve调用:execve(“hello”,NULL,NULL)。
execve加载和运行hello程序会经过以下步骤:

  1. 删除已存在的用户区域:这里指在fork后创建于此进程用户区域中的shell父进程用户区域副本。
  2. 映射私有区域:为hello程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射到hello可执行文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
  3. 映射共享区域:hello程序与一些共享对象或目标链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC):设置此进程上下文中的程序计数器,使之指向hello代码区域的入口点。

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

DRAM缓存不命中称为缺页。缺页故障属于异常类别中的故障,是潜在可恢复的错误。发生一个缺页异常后,控制会转移到内核的缺页处理程序。接着会判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。若合法,该程序会选择一个牺牲页,如果牺牲页已经被修改了,内核会将其复制回磁盘。随后内核从磁盘复制引发缺页异常的页面至内存,更新对应的页表项指向这个页面。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。
计算机系统大作业 程序人生-Hello’s P2P_第60张图片
图 7.3 缺页异常处理过程

7.9动态存储分配管理

动态内存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种风格——显式分配器和隐式分配器。
(1)显式分配器要求应用显式地释放任何已分配的块。C语言中的malloc程序包就是一种显式分配器。显式分配器必须在一些相当严格的约束条件下工作:①处理任意请求序列;②立即响应请求;③只使用堆;④对齐块;⑤不修改已分配的块。在以上限制条件下,分配器要最大化吞吐率和内存使用率。
(2)隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。
常见的放置策略有:
(1)首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块。
(2)下一次适配:类似于首次适配,但从上一次查找结束的地方开始搜索。
(3)最佳适配:选择所有空闲块中适合所需请求大小的最小空闲块。
组织内存块的方法包括:
(1)隐式空闲链表:空闲块通过头部中大小字段隐含连接,可添加边界标记提高合并空闲块的速度。
(2)显式空闲链表:在隐式空闲链表块结构的基础上,在每个空闲块中添加一个pred(前驱)指针和一个succ(后继)指针。
(3)分离的空闲链表:将块按块大小划分大小类,分配器维护一个空闲链表数组,每个大小类一个空闲链表,减少分配时间同时也提高了内存利用率。C语言中的malloc程序包采用的就是这种方法。

7.10本章小结

本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理的知识,我了解了现代计算机系统为提高内存存储效率和使用率所做的大量工作,同时我也对一直以来不太理解的malloc机制有了更深刻的理解。

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

  1. 设备的模型化:文件。
    所有的I/O设备(如内核、网络、磁盘和终端等)都被模型化为文件。
  2. 设备管理:Unix IO接口。
    这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等

8.2 简述Unix IO接口及其函数

8.2.1 Unix IO接口

(1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要
访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描
述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h> 定义了常量STDIN_FILENO、STOOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置
k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件
位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测这个条件。在文件末尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文
件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

8.2.1 Unix IO函数

  1. open()函数:
    函数原型:int open(char *filename, int flags, mode_t mode);
    open函数用来打开一个存在的文件或是创建一个新文件的,open函数将filename转换为一个文件描述符,并且返回描述符数字(总是在进程中当前没有打开的最小描述符),flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。返回值表示是否成功。
  2. close()函数:
    函数原型:int close(int fd);
    close函数关闭一个打开的文件。返回值表示是否成功。
  3. read()函数:
    函数原型:ssize_t read(int fd, void *buf, size_t n);
    read函数是从指定文件中读数据。该函数从文件描述符为fd的当前文件位置复制n个字符到缓冲区buf。返回值表示的是实际传送的字节数量,若为-1则表示出错,为0表示读到EOF。
  4. write()函数:
    函数原型:ssize_t write(int fd, const void *buf, size_t n);
    write函数是向指定文件中写数据。该函数从缓冲区buf复制n个字符到文件描述符fd的当前位置。返回值表示是否成功。
  5. lseek()函数:
    函数原型:off_t lseek(int fd, off_t offset,int whence);
    Lseek函数用于在指定的文件描述符中将文件指针定位到相应位置,进而显式地修改当前文件的位置。返回值表示是否成功。

8.3 printf的实现分析

先查看printf函数(如下图8.1),可以发现printf函数调用了vsprintf函数,最后通过调用函数write函数进行输出;形参列表中的…表示传递参数的个数不确定。va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。

计算机系统大作业 程序人生-Hello’s P2P_第61张图片
图8.1 printf函数
接着再来看vsprintf函数体:
计算机系统大作业 程序人生-Hello’s P2P_第62张图片
图8.2 vsprintf函数
我们可以看到,vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。在printf中调用函数write把buf中的i个元素的值写到终端。printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出。
下面我们来追踪write:
计算机系统大作业 程序人生-Hello’s P2P_第63张图片
图8.3 write追踪
这里通过几个寄存器进行传参,随后调用中断门INT_VECTOR_SYS_CALL,即通过系统来调用sys_call实现输出这一系统服务。
计算机系统大作业 程序人生-Hello’s P2P_第64张图片
图8.4 sys_call
通过逐个字符直接写至显存,输出格式化的字符串。 syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序实现从ASCII到字模库到显示vram(即显存存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

当程序调用getchar时,用户输入的字符被存放在缓冲区中,当用户键入回车之后(字符串和回车都保存在缓冲区内),getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出现错误返回-1。如用户在按回车之前输入了不止一个字符,其他字符会继续保留在缓存区中,等待后续getchar调用读取。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的实现有了更深刻的理解。

(第8章1分)

结论

一、 hello所经历的过程
hello的C程序虽然简单,但它的一生却十分复杂,与计算机系统密不可分。具体过程包括:

  1. hello.c通过预编译期进行预编译展开,获得文本文件hello.i。
  2. hello.i通过编译器进行编译,获得汇编文件hello.s。
  3. hello.s通过汇编器进行汇编,得到可重定位目标文件hello.o。
  4. hello.o通过链接器进行链接,生成了可执行目标文件hello。
  5. 当用户在shell-bash中输入运行hello程序的命令时,shell-bash会对输入的命令行进行分析解释,找到可执行目标文件hello。
  6. 进程调用fork函数,生成子进程,调用execve函数加载并运行当前进程的上下文并加载运行进程hello,hello程序开始执行。
  7. 在hello作为进程运行时,会受到进程调度,特别是调用到sleep函数时,会充分利用资源。
  8. hello的运行过程不是一帆风顺的,他也会收到各种可能的异常,可能是来自外部的“破坏”(键盘输入)或者内部的故障、陷阱、信号等,这些都需要内核协同进行处理。
  9. hello的运行过程中会出现线性地址、虚拟地址、逻辑地址、物理地址。
  10. hello在运行时会调用printf,getchar等函数,这些函数与Linux系统的I/O设备管理、Unix IO接口相关
  11. hello程序结束后,会被shell父进程回收,也会清除在内存中的各种数据结构和信息。hello被“宣告死亡”,结束了它的一生。

二、 个人感悟

计算机系统和计算机底层的知识联系得十分紧密,虽然这门课程内容繁多,但
有一条比较明晰的主线,逻辑十分清晰,从这次“hello的一生”大作业中就能看出一个小小的hello程序就能够充分展现出计算机系统的各个方面。计算机系统这门课程彼此之间联系十分紧密,刚开始学习这门课的时候,感觉有一些知识很难理解,但逐渐学到后面的部分才能更好地理解前面的知识。
(结论0分,缺失 -1分,根据内容酌情加分)

附件

文件名称 作用 对应本文章节
hello.i hello.c经预处理得到的文件 第2章
hello.s hello.i经编译得到的汇编代码文本文件 第3章
hello.o hello.s经汇编得到的可重定位目标文件 第4章
elf.txt hello.o经readelf分析得到的文本文件 第4章
hello.asm hello.o经objdump反汇编得到的文本文件 第4章
hello hello.o经链接得到的可执行文件 第5章
hello.elf hello经readelf分析得到的文本文件 第5章
hello2.asm hello经objdump反汇编得到的文本文件 第5章
计算机系统大作业 程序人生-Hello’s P2P_第65张图片

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统(原书第三版).机械工业出版社, 2016.
[2] GCC online documentation. http://gcc.gnu.org/onlinedocs/
[3] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].
https://www.cnblogs.com/pianist/p/3315801.html.
[4] ELF文件头结构. CSDN博客.
https://blog.csdn.net/king_cpp_py/article/details/80334086
[5] Linux可执行文件格式-ELF结构详解
https://www.cnblogs.com/QiQi-Robotics/p/15573352.html
[6] printf函数实现的深入剖析. 博客园.
https://www.cnblogs.com/pianist/p/3315801.html
[7] x86-3-段式管理(segmentation)
https://www.cnblogs.com/Sna1lGo/p/15786602.html
[8] read和write系统调用以及getchar的实现. CSDN博客.
https://blog.csdn.net/ww1473345713/article/details/51680017

(参考文献0分,缺失 -1分)

你可能感兴趣的:(linux)