计算机系统大作业

在这里插入图片描述

计算机系统大作业

题 目 程序人生-Hello’s P2P

专 业 计算机科学与技术

学  号 2022111385

班 级 2203102

学 生 乔思远

指 导 教 师 史先俊

计算机科学与技术学院

2023年12月

摘 要

hello作为每位程序员的第一个程序,它有着朴素的“外衣”,但它的一生并不像我们看到的那么简单。为帮助程序员了解hello“外衣”里面的内容,本文从hello的程序代码出发,一步步介绍了hello一生中涉及的计算机系统的知识与内容,据此更加深入地了解计算机系统内部的运行机制,这对于程序员的编程具有重要意义。

关键词:hello;程序的一生;P2P;O2O

目 录

文章目录

第1章 概述

1.1 Hello简介

P2P:

hello生命进程开始于文本编辑器中,程序员将其代码输入并保存在.c格式的文本文件中(Program)。以Linux系统为例,hello从高级C程序到可执行文件要经过四个阶段:预处理阶段,预处理器(cpp)根据以字符#开头的命令,修改原始的hello程序,得到一个新的文本文件(通常格式为.i);编译阶段,编译器(ccl)将预处理得到的文本文件翻译成了汇编语言程序,其同样是一个文本文件(通常格式为.s);汇编阶段,汇编器(as)将汇编语言程序翻译为机器指令,生成可重定位目标程序文件(通常格式为.o);链接阶段,由于程序使用了C语言库中的printf,exit,getchar等函数,需要使用链接器(ld)合并它们,结果得到了一个可执行目标文件。这时在shell中调用相关命令便会为hello程序创建一个进程(Process)。以上就是hello的P2P过程。

在这里插入图片描述

图1.1-1 流程

O2O

在shell中运行hello的可执行目标文件会为其创建子进程,并调用execve函数加载hello程序,系统为其分配虚拟内存,CPU开始执行指令。当程序执行完成后,父进程会对该子进程进行回收,子进程在这一时刻不复存在了。

1.2 环境与工具

硬件环境

处理器 12th Gen Intel® Core™ i5-12500H 2.50 GHz
系统 64 位操作系统, 基于 x64 的处理器
RAM 16.0 GB

软件环境:Windows 11,VMware Workstation 16 pro,Ubuntu 20.04.6

开发调试工具:gdb,edb,vim,gcc,Visual Studio 2022,CodeBlocks

1.3 中间结果

hello.c:源代码文件

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

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

hello.o:汇编后的可重定位目标文件

hello:可执行文件

hello.asm:hello的反汇编代码

hello.o.asm:hello.o的反汇编代码

hello.elf:hello的ELF信息

hello.o.elf: hello.o的ELF信息

1.4 本章小结

本章对hello的P2P和O2O过程,实验使用的软硬件条件和实验中产生的文件进行了简要的概述。

第2章 预处理

2.1 预处理的概念与作用

2.1.1概念

预处理器(cpp)处理以字符#开头的命令,对源程序相关内容进行替换,生成另一个C程序。

2.1.2作用

预处理的功能主要有处理头文件、宏定义、条件编译、注释内容。下面分别进行介绍:

(1)处理头文件。在预处理时,预处理器会用指定文件中的内容替换当前代码行。例如hello.c中的#include <stdio.h>预处理器会把stdio.h里面的内容全都复制在此处。这么做可以节省大量时间,有效避免无用的工作;

(2)处理宏定义。在预处理时,预处理器会用真实值替换宏定义的符号。例如#define M 10,预处理器就会将代码中所有M替换为10(如果代码中没有#undef命令)。这么做可以提高代码的可用性和修改效率,若想对M的值进行修改,只需更改一处;

(3)处理条件编译。如果代码中有#ifdef等代码,预处理器在预处理时也会对其进行处理。这么做可以让编译器只编译程序中满足要求的代码段,从而达到了节省时间空间的目的。

(4)处理注释内容。预处理器也会对注释内容进行删除。

2.2在Ubuntu下预处理的命令

预处理命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -E hello.c -o hello.i
在这里插入图片描述
图2.2-1 预处理命令
在这里插入图片描述
图2.2-2 文件列表
在这里插入图片描述
图2.2-3 hello.i文件

2.3 Hello的预处理结果解析

hello.i文件一共有3901行,大致可以分为四个部分(前三部分是头文件或其间接引用的内容)。
首先是头文件中间接引用的文件路径,如下图所示:
在这里插入图片描述
图2.3-1 hello.i中外部库路径(部分)

然后是一些类型的重命名和结构体的定义。
在这里插入图片描述
图2.3-2 hello.i文件中结构体的定义(部分)
在这里插入图片描述
图2.3-3 hello.i文件中类型的重命名(部分)

其次是一些外部函数的引入。
在这里插入图片描述
图2.3-4 hello.i文件中外部函数的引入(部分)

最后是hello的C语言程序。
在这里插入图片描述
图2.3-5 hello.i文件中的C语言程序

2.4 本章小结

本章对预处理阶段进行了详细介绍,从预处理的概念作用到hello.i文件的介绍。hello.i文件与hello.c文件相比,内容复杂繁琐,这也进一步说明了预处理的重要性,可以为程序编写节省大量时间,这时的hello才具备成功运行的可能。

第3章 编译

3.1 编译的概念与作用

3.1.1概念

编译阶段,编译器(ccl)会将hello.i翻译成汇编语言程序hello.s。汇编代码以文本格式描述低级机器语言指令。编译过程可以分为6个阶段,词法分析,语法分析,语义分析,中间代码生成,代码优化和目标代码生成。

3.1.2作用

编译阶段,编译器将高级程序语言翻译为统一的,接近机器语言,对机器友好的汇编代码,并且汇编代码既可以让我们近距离的观察机器代码,又以一种可读形式表示。

3.2 在Ubuntu下编译的命令

编译命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -S hello.i -o hello.s

在这里插入图片描述

图3.2-1 编译命令

在这里插入图片描述

图3.2-2 文件列表

在这里插入图片描述

图3.2-3 hello.s文件

3.3 Hello的编译结果解析

3.3.1数据

3.3.1.1常量

(1)程序中用到了字符串常量“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”,它们分别存储在.LC0,.LC1所标记的位置,如图3.3.1.1-1所示。第一个字符串输出时,到.LC0所标记的位置读取,由于printf中不含其他的格式化参数,所以编译器会将其优化为puts函数,如图3.3.1.1-2所示。同理,第二个字符串输出时到.LC1标记的位置读取,调用printf函数进行输出,如图3.3.1.1-3所示。

在这里插入图片描述

图3.3.1.1-1 字符串常量

在这里插入图片描述

图3.3.1.1-2 第一个字符串输出

在这里插入图片描述

图3.3.1.1-3 第二个字符串输出

(2)程序中还是用到了数字常量4和8,如图3.3.1.1-4所示。它们在汇编代码中的表示如图3.3.1.1-5所示。

在这里插入图片描述

图3.3.1.1-4 源程序中的整型常量

在这里插入图片描述
在这里插入图片描述

图3.3.1.1-5 汇编中的整型常量

3.3.1.2变量

程序中使用了局部变量i,它的存储位置为%ebp,如下图所示。

在这里插入图片描述

图3.3.1.2-1 汇编中的变量

3.3.2类型转换

(1)程序中使用了atoi函数。编译阶段,编译器对程序进行了优化,将atoi函数替换为例strtol函数。对于该函数的参数传递,查找资料得知atoi()与使用strtol(nptr,(char**)NULL,10)结果相同,从而得到了合理的解释。

在这里插入图片描述

图3.3.2-1 atoi函数

在这里插入图片描述

图3.3.2-2 strtol函数的参数传递

(2)隐式类型转换。atoi函数的返回值类型为int,而sleep函数要求参数类型为unsigned int,从而这里存在隐式类型转换。但汇编代码中找不到体现。原因也很好理解,只是数据二进制的解释形式发生了变化。

在这里插入图片描述

图3.3.2-3 隐式类型转换

3.3.3赋值

hello程序中使用了i=0赋值操作,它在汇编代码中的表示如下图。

在这里插入图片描述

图3.3.3-1 汇编中的赋值操作

3.3.4算数操作

hello程序中使用了i++算数运算操作,它在汇编代码中的表示如下图。

在这里插入图片描述

图3.3.4-1 汇编中的算数操作

3.3.5关系操作

hello程序中出现了多次关系操作,如图3.3.5-1所示。它们在汇编中的表示如图3.3.5-2。

在这里插入图片描述

图3.3.5-1 源程序中的关系操作
在这里插入图片描述
在这里插入图片描述

图3.3.5-2 汇编中的关系操作

3.3.6数组/指针操作

hello中使用了argv数组,其中存放的是字符串的首地址(存放的是指针)。其在汇编中的表示如图3.3.6-1。从中可以看出,由于%rbx中存储的是argv数组的首地址,从而(%rax)就相当于对存储地址的解引用,进而通过加减8的整数倍可以得到各个字符串的首地址(64位系统)。

这里可以观察到多出来一个参数传递,经过查找得知由于调用的函数为__printf_chk,第一个参数是为了防止栈溢出。

在这里插入图片描述

图3.3.6-1 argv数组在汇编中的表示

3.3.7控制转移

(1)程序中使用了if/else语句,相关源代码和汇编代码如下图所示。

在这里插入图片描述

图3.3.7-1 if/else语句

在这里插入图片描述

图3.3.7-2 if/else语句的汇编代码

(2)程序中还使用了for循环,相关源代码和汇编代码如下图所示。

在这里插入图片描述

图3.3.7-3 for循环语句

在这里插入图片描述

图3.3.7-4 for循环汇编代码

3.3.8函数调用

程序调用了printf函数,exit函数,atoi函数,sleep函数和getchar函数,下面在源代码和汇编代码中对它们进行了标注。由于编译器会对代码进行优化,所以汇编代码中出现的函数与源程序有出入。例如编译器将atoi函数优化为功能更强大,更安全的strtol函数。

在这里插入图片描述

图3.3.8-1 源代码中的函数调用

在这里插入图片描述

图3.3.8-2 汇编代码中的函数调用

3.4 本章小结

本章介绍了编译的概念作用。然后,使用带编译选项的shell命令,在Ubuntu下对hello.i进行编译,得到了hello.s文件。

最后,根据ppt中列出的C语言的数据与操作,根据hello.c程序中出现的数据、类型转换、赋值、算术操作、关系操作、数组/指针操作、控制转移和函数调用,对比分析了源程序和汇编代码。阅读汇编代码有助于程序员加深对底层程序运行的理解,进而在提高程序效率等方面做出改进。

第4章 汇编

4.1 汇编的概念与作用

4.1.1概念

汇编阶段,汇编器(as)将汇编代码翻译为了二进制的机器语言,并将这些机器语言打包为可执行可链接格式(ELF),存储在可重定位目标文件中(通常格式为.o)。

4.1.2作用

在汇编阶段,hello实现了从汇编程序到二进制机器语言的转换,机器语言也是机器可以真正理解的格式。

4.2 在Ubuntu下汇编的命令

编译命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -c hello.s -o hello.o

在这里插入图片描述

图4.2-1 汇编命令

在这里插入图片描述

图4.2-2 文件列表

4.3 可重定位目标elf格式

4.3.1整体介绍

在详细分析hello.o的ELF格式前,先整体介绍一下ELF目标文件格式,如图4.3-1所示(截取自课件)。ELF在解析时可以分为两种视图:链接视图和执行视图。链接视图以节为单位,段头表(程序头表)是可选的,节区头部表必须有;而执行视图以段为单位,段头表(程序头表)必须有,节区头部表可选。

在这里插入图片描述

图4.3.1-1 ELF文件格式

4.3.2 ELF头

使用readelf -h hello.o命令查看hello.o的ELF头,得到如下结果。ELF头以Magic开始,Magic的前四个字节表示这是一个ELF文件(0x7f是固定开头,0x45 0x4c 0x46是ELF的ASCII码值),后面的0x02表示这个文件是64位架构,第一个0x01表示文件为小端序,第二个0x01表示版本号,当然这里只显示了hello.o中部分字节,我们可以使用hexdump工具显示之后的内容,如图4.3.2-2所示。

ELF头之后都是一些基本信息,比如字节顺序,机器类型,文件类型等等。我们可以在/usr/include/elf.h文件中找到ELF结构体的定义,如图4.3.2-3所示。

在这里插入图片描述

图4.3.2-1 hello.o的ELF头

在这里插入图片描述

图4.3.2-2 hexdump查看hello.o

在这里插入图片描述

图4.3.2-3 elf.h文件

4.3.3节头表

使用readelf -S --wide hello.o命令查看节头表中的内容。由于此时为链接视图,从而我们可以从节头表中看到ELF中所有节的信息。图中对Type进行标注。

在这里插入图片描述

图4.3.3-1 hello.o的节头表

下面对节头表中的信息进行详细解读(elf.h中也有节头表的结构体定义)。

Name本节的名称
Type根据本节的内容和语义对节的种类进行划分
Address这个节被加载后的虚拟地址
Off本节在文件中的偏移量
Size本节的大小
ES指明该节对应的每一个表项的大小
Flg本节的访问权限(可读/可写/可执行等)
Lk指向节头表中本节所对应的位置
Inf指明该节的附加信息
Al本节的对齐方式

表4.3.3-1 节头表中信息解读

在这里插入图片描述

图4.3.3-2 elf.h文件

4.3.4重定位节

当汇编器生成 hello.o 后,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的外部符号的位置。所以,当汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.text中。

使用readelf -r hello.o命令查看hello.o的重定位节,发现它有两个重定位节。

在这里插入图片描述

图4.3.4-1 hello.o的重定位节

首先介绍偏移量和加数。前者是需要被修改引用的节偏移量,后者是汇编器根据重定位类型等信息设定的值。

然后介绍类型。观察发现这里的重定位条目类型有三种。R_X86_64_32意思是重定位一个使用32位绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改;R_X86_64_PC32意思是重定位一个使用32位PC相对地址的引用。通俗理解就是根据目标与程序计数器当前值的差值进行寻址;R_X86_64_PLT32意思是过程链接表(PLT)延迟绑定。查找了解到R_X86_64_PLT32重定位类型是用于将程序中对于动态链接库中函数调用地址重定位到PLT中的一种重定位类型。

4.3.5符号表

符号表中包含hello中定义和引用的符号的信息。使用readelf -s hello.o命令查看。

在这里插入图片描述

图4.3.5-1 hello.o的符号表

Value是距定义目标的节的起始位置的偏移量;Size为目标的大小;Type是指符号的类型,通常要么是数据,要么是函数;Bind表示符号是本地的还是全局的;Vis表示符号的可见性;Ndx表示节,因为readelf用一个整数索引来标识每个节,比如Ndx=1表示.text节;Name表示名称。

4.4 Hello.o的结果解析

使用objdump -d -r hello.o > hello.o.asm命令得到hello.o的反汇编如下(这里为了方便查看,将反汇编代码输出到了hello.o.asm文件中)。发现使用objdump自动把重定位条目加入了汇编代码中。

在这里插入图片描述

图4.4-1 hello.o的反汇编代码

将hello.o.asm与第3章的 hello.s进行对比。发现在以下几方面不同。

(1)分支转移不同。hello.s中使用了.L2,.L3和.L6作为跳转标记;而在hello.o.asm中使用地址(不是真实的内存地址)来跳转,如下图所示。

在这里插入图片描述

图4.4-2 分支转移的不同

(2)函数调用不同。在hello.s中直接使用函数的名字来进行调用;在hello.o.asm中使用偏移量来进行调用(调用函数距下一条指令的距离),只不过这里还没有进行重定位,所以用0来进行占位。

在这里插入图片描述

图4.4-3 函数调用的不同

(3)操作数的进制不同。在hello.o中操作数为十进制;在hello.o.asm中操作数的进制为十六进制。

在这里插入图片描述

图4.4-4 操作数进制不同

(4)有无汇编指示符。在hello.o中有汇编指示符;在hello.o.asm中没有汇编指示符。

在这里插入图片描述

图4.4-5 汇编指示符

4.5 本章小结

本章介绍了汇编的概念和作用,只有经过汇编阶段后,程序才变成机器可以识别的样子。然后通过gcc命令得到了hello.o可重定位目标文件,并利用readelf命令对其各个部分进行了详细的解读。最后得到hello.o的反汇编代码,将其与hello.s文件内容做对比,发现了两者间的不同。

第5章 链接

5.1 链接的概念与作用

5.1.1概念

链接是将各种代码和数据片段收集并组合为单一文件的过程,由链接器来自动完成,这个单一文件是可以被加载到内存执行的。链接可以执行于编译时,也可以执行于加载时,甚至可以执行于运行时。

5.1.2作用

链接主要分为两个步骤,符号解析和重定位。符号解析是将每个引用与输入的可重定位目标文件符号表的符号关联起来;重定位是指在合并出聚合节后,链接器修改每个符号的引用,让它们指向正确的运行时的地址。链接的存在使得分离编译成为可能。它使得程序员可以对软件进行模块化设计,然后模块化编程,这样分组工作很高效。而且,需要修改或者调试时,只需要修改某个模块,然后简单地重新编译它,并重新链接,而不必重新编译其他文件。

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.2-1 链接命令

在这里插入图片描述

图5.2-2 文件列表

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

使用readelf -a hello > hello.elf和readelf -a hello.o > hello.o.elf命令得到hello和hello.o的ELF描述。对比发现hello.o.elf中多了程序头表,下面将对其简要介绍,其余部分与hello.o类似不再赘述。从节头表中可以看到各段的基本信息,包括各段的起始地址,大小等信息。

在这里插入图片描述

图5.3-1 hello和hello.o的ELF头

在这里插入图片描述

图5.3-2 hello和hello.o的节头表

在这里插入图片描述

图5.3-3 hello和hello.o的重定位节

在这里插入图片描述

图5.3-4 hello和hello.o符号表

程序头表中存储着页面大小,虚拟地址等信息。观察程序头表,可以发现有九种程序头。Offset指偏移量,Flags指访问权限,Align指对齐方式,即每个段的起始地址%align要等于偏移量%align。

在这里插入图片描述

图5.3-5 hello的程序头表

5.4 hello的虚拟地址空间

在edb中加载可执行程序hello,如下所示。

在这里插入图片描述

图5.4-1 edb加载hello

在edb中打开Memory Regions,查看每块内存的读写权限。

在这里插入图片描述

图5.4-2 edb中的Memory Regions

这里选取.interp和.rodata进行对照分析。从节头中我们可以得到.interp的起始地址为0x4002e0,大小为0x1c;.rodata的起始地址为0x40200,大小为0x3b,如下图所示。

在这里插入图片描述

图5.4-3 节头表

在edb中找到对应内存,查看内容如下。图中框选的内容就是对应段的内容。

在这里插入图片描述

图5.4-4 .interp内容

在这里插入图片描述

图5.4-5 .rodata内容

5.5 链接的重定位过程分析

5.5.1 hello.asm和hello.o.asm文件对比

为方便比较分析,使用命令objdump -d -r hello > hello.asm,将反汇编代码存入hello.asm文件中,如下图所示。

在这里插入图片描述

图5.5.1-1 hello可执行程序的反汇编代码

经过对比发现,hello.asm和hello.o.asm主要在以下几方面存在差异。

(1)可重定位信息被修改。hello.asm中已经没有可重定位条目,如下图所示。这些可重定位条目包括分支转移,函数调用,字符串常量的使用等。

在这里插入图片描述

图5.5.1-2 可重定位条目

(2)两个文件的大小不同。hello.asm有188行,hello.o.asm只有51行。hello.asm中插入了共享库函数的指令,如下图所示。

在这里插入图片描述

图5.5.1-3 共享库函数

5.5.2链接过程

正如前文中所说,链接分为两步:符号解析和重定位。

符号解析是将每个出现的符号与输入的可重定位目标文件符号表的符号关联起来,若在符号表中没有找见相应符号,链接器会报错;

重定位又分为两步。第一步重定位节和符号定义,在这一步骤中,链接器会整合所有可重定位目标文件,将所有相同类型的节整合为同一类型的聚合节;第二步重定位符号引用,此时链接器修改每个符号的引用,让它们指向正确的运行时的地址。重定位符号引用又可主要分为重定位PC相对引用和重定位绝对引用。

5.5.3重定位过程分析

(1)先介绍重定位绝对引用。以hello.o.asm中的一个可重定位条目为例。如下图所示。此例的重定位类型为R_X86_64_32,.rodata.str1.8位于0x402008,所以将占位的0直接替换为0x402008,机器为小端序。

在这里插入图片描述

图5.5.3-1 重定位绝对引用

(2)再介绍重定位PC相对引用。同样以hello.o.asm中的一个可重定位条目为例。如下图所示。此例中的重定位类型为R_X86_64_PLT32,观察hello.asm,我们可以知道puts函数的绝对地址(r.symbol)为0x401090,refaddr为0x401144,r.addend为-4,从而得出替换值为r.symbol+r.addend-refaddr=0xffffff48(补码)。

在这里插入图片描述

图5.5.3-2 重定位PC相对引用

5.6 hello的执行流程

在edb中打开hello程序,设置edb从Application Entry Point处进入调试。

在这里插入图片描述

图5.6-1 设置Application Entry Point

然后运行hello程序,程序会在0x4010f0处停止,这与ELF头中的程序入口地址相吻合,此时观察edb寄存器栏,从中我们可以得到当前所处的函数。

在这里插入图片描述

图5.6-2 运行时查看当前函数

不断单步运行hello程序,得到各主要函数的地址,如下所示。

函数名地址函数名地址
_start0x4010f0__libc_start_main0x00007f9a8cb9bf90
__cxa_atexit0x00007f9a8cbbede0__libc_csu_init0x00000000004011b0
_setjmp0x00007f9a8cbbac80main0x0000000000401125
__printf_chk0x4010b0strtol0x4010a0
sleep0x4010d0getc0x4010e0
exit0x00007f521a771a40puts0x401090
hello!exit@plt0x4010c0

表5.6-1 各函数地址

根据执行流程绘制如下图。

在这里插入图片描述

图5.6-3 流程图

5.7 Hello的动态链接分析

当程序调用一个由共享库定义的函数时,由于编译器无法预测这时候函数的地址是什么,因为定义它的共享模块在运行时可以加载到任意位置。为此,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。通过观察.got.plt节的变化,就能观察到动态链接的过程。

通过观察节头表可以得到.got.plt的位置和大小。.got.plt节的起始位置为0x404000,大小为0x48。

在这里插入图片描述

图5.7-1 .got.plt节的位置和大小

在edb中查看对应位置。发现有两个字节发生了变动。第一个字节存储的是动态链接器在解析函数地址时会使用的信息;第二个字节是动态链接器ld-linux.so模块中的入口点。

在这里插入图片描述

图5.7-2 .got.plt对比

5.8 本章小结

本章中介绍了链接的概念作用,并在虚拟机上进行链接得到了hello可执行程序。然后对比分析了hello和hello.o的ELF格式,对新出现的程序头表进行了介绍,并在edb中查看了虚拟地址空间各段信息。之后又对比了hello和hello.o的反汇编程序,并结合hello.o的重定位项目对hello的可重定位过程进行了分析,包括重定位PC相对寻址和重定位绝对寻址。最后利用edb工具对hello的执行流程和动态链接进行了分析,加深了对hello的理解。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1概念

进程的定义是一个正在运行的程序实例,系统中的每个程序都运行在某个进程的上下文中。

6.1.2作用

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

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

6.2.1 Shell的作用

shell 是一个应用程序,它连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核,这就是 shell 的本质。shell最重要的功能是命令解释,它识别用户输入的指令并将它们送到内核。用户可以通过shell访问操作系统内部的服务。

6.2.2 Shell的处理流程

当用户提交了一个命令后,shell首先判断它是否为内置命令,如果是就通过shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序,shell就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。

执行外部命令时,若执行的进程为前台进程,shell创建会通过fork函数创建一个子进程并阻塞命令的输入直到前台进程终止运行。在子进程中调用execve函数加载并运行可执行目标文件,当子进程执行结束时在信号处理程序中用waitpid函数对其进行回收。若执行的进程为后台进程,shell会直接弹出命令提示符,不等待子进程结束。

6.3 Hello的fork进程创建过程

当我们在shell中输入命令./hello时,shell会先判断这个命令是不是内置命令,然后把其当作一个可执行程序的名字,若在当前路径中找见了这个文件就开始执行它。之后shell会调用fork函数,创建一个子进程,hello将会在这个进程中执行。

这里再简单介绍以下fork函数,fork函数调用一次,返回两次,在父进程中返回子进程PID,在子进程中返回0。这一点经常被用来区分它们。fork函数创建的子进程几乎但不完全与当前进程相同。子进程将得到与当前进程虚拟地址空间相同但独立的一份副本,包括代码,数据段,堆,共享库以及用户栈,甚至于父进程打开的文件的描述符。然后利用这个副本执行子进程。子进程与父进程最大的不同是PID不同。

6.4 Hello的execve过程

只让子进程运行同父进程相同的内容多少有点无聊,如果我们想让子进程运行新的内容,该怎么做呢?这时就要用到execve函数。它的作用是加载并运行指定的可执行目标文件,在加载运行成功的前提下,它不会返回。它调用启动代码,启动代码会设置栈,并将控制传递给新程序的主函数。注意,execve函数并不会改变进程的PID。

以hello可执行程序为例,通过fork函数创建子进程后,在子进程中使用execve函数加载运行hello,execve函数会调用内核提供的启动代码。内核会将原上下文替换为hello的上下文,然后将控制传递给新程序的主函数。

6.5 Hello的进程执行

这里涉及到进程的关键抽象,独立的逻辑控制流。由于操作系统同时会运行多个进程,所以处理器的一个物理控制流就会分成了多个逻辑控制流,来交替执行这些进程,给程序一种独占处理器的假象。一个进程执行它的控制流的一部分的每一段时间叫做时间片。当开始hello开始执行时,操作系统会为其分配时间片。

在这里插入图片描述

图6.5-1 逻辑控制流(教材图)

进一步,操作系统内核使用一种成为上下文切换的异常控制流来实现多任务(多进程)。上下文就是内核重新启动一个被抢占的进程所需要的状态,包括寄存器,程序计数器等内容。如CPU正在执行hello中某条指令,操作系统认为hello进程了运行足够久(或执行到sleep函数)。在这时候,程序将由用户模式转换成内核模式,内核中的调度器执行上下文切换,保存当前的上下文信息,恢复某个先前被抢占的进程被保存的上下文,然后再由内核模式转换至用户模式,将控制传递给这个先前被抢占的进程。

在这里插入图片描述

图6.5-2 进程上下文切换(教材图)

6.6 hello的异常与信号处理

异常控制流可以分为四种:异常,进程,信号和非本地跳转。异常位于硬件与操作系统交界的部分,可分为中断,陷阱,故障和终止;信号位于应用和操作系统的交界之处。由于异常和信号数量很多,这里抽取几个进行说明。

6.6.1正常运行

在shell中输入命令./hello 2022111385 乔思远 1,程序执行结果如下。程序运行后每隔1秒输出一个Hello 2022111385 乔思远。在输出8个之后,程序会等待用户输入一个字符,读取到字符后程序终止。

在这里插入图片描述

图6.6.1-1 程序正常运行结果

6.6.2乱按键盘

在程序执行时乱按键盘,程序不会受任何影响。当在shell中输入命令./hello 2022111385 乔思远 1后,shell会将hello作为前台进程开始执行。在此期间,shell对乱按键盘输入的东西不会做任何处理,直到hello进程结束,shell才会对输入的字符串进行解析(如果输入回车),如下图所示。

在这里插入图片描述

图6.6.2-1 乱按键盘

6.6.3 Crtl-Z

当程序运行时输入Crtl-Z的结果如下所示。当键盘输入Crtl-Z时,shell会接收到SIGTSTP信号,并且shell会将这个信号转发给前台进程组中的所有进程,挂起前台进程组。

在这里插入图片描述

图6.6.3-1 输入Crtl-Z后程序运行结果

使用ps和jobs命令进行查看,得到如下结果。

在这里插入图片描述

图6.6.3-2 jobs和ps命令执行结果

再使用pstree命令查看。pstree是一个Linux下的命令,通过它可以列出当前的进程,以及它他们的树状结构。

在这里插入图片描述

图6.6.3-3 pstree命令执行结果

为让停止的前台进程组继续运行,我们可以输入fg %1或fg 1(%可有可无)让停止的hello继续运行,如下所示。

在这里插入图片描述

图6.6.3-4 fg命令执行结果

在hello进程停止后,可以使用kill命令向其发送信号,比如:终止信号(SIGINT),发送SIGINT信号后,ps中就看不到hello进程了。每种信号都对应一个序号,9对应着SIGINT,9291是hello进程的PID。补充:若在PID前加上负号,信号会发送到进程组PID中的每个进程。

在这里插入图片描述

图6.6.3-5 kill内置命令

6.6.4 Crtl-C

当程序运行时从键盘输入Crtl-C,程序运行结果如下。此时shell会接收到SIGINT信号,并且shell会将这个信号转发给前台进程组中的所有进程,终止前台进程组。之后使用ps和jobs命令进行查看。

在这里插入图片描述

图6.6.4-1 Crtl-C

6.7本章小结

本章首先介绍了进程的概念作用,壳shell-bash的作用与处理流程。然后,结合hello的fork过程,execve过程和进程执行流程进一步说明了shell的处理过程和操作系统的上下文切换,逻辑控制流等概念。最后,通过在hello执行过程中按Ctrl-Z或Ctrl-C或不停乱按,对异常控制流进行了简要说明。

第7章 hello的存储管理

7.1 hello的存储器地址空间

linux系统非常有限的使用分段管理,导致逻辑地址,线性地址和虚拟地址的数值是一样的。

7.1.1逻辑地址

逻辑地址是用户编程时使用的地址,分为段地址和偏移地址两部分,它的格式为:[段地址:段内偏移]。edb中的地址表示就是逻辑地址。因此在Linux系统下,逻辑地址也就是线性地址,因为Linux系统中各段的起始地址均为0x0。hello的反汇编代码中的地址可以理解为逻辑地址。

7.1.2线性地址

非负整数地址的有序集合。是逻辑地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。hello的反汇编代码中的地址可以理解为线性地址。

7.1.3虚拟地址

虚拟地址是现代操作系统提供的一种内存管理的抽象。在Linux中虚拟地址在数值上等同于线性地址。其经过MMU处理可以变为物理地址。

7.1.4物理地址

主存被组织成一个由M个连续的字节大小的单元组成的数组,其中每个字节都被赋予了一个唯一的物理地址。指令的读取和数据的访问都要通过物理地址来进行访问。

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

下面的讲述都是基于Intel 32位情况下的。

由上文可知,逻辑地址由两部分构成,段地址和段内偏移。逻辑地址一共有48位,其前16位被称为段选择子,它被用来得到段地址,这16位的格式如下。其中索引为描述符表的索引(注意在找位置的时候要乘8);TI用于区分全局描述符表(GDT)和局部描述符表(LDT)。若TI为0,描述符表为GDT,若TI是1,则描述符表为LDT;请求特权级(RPL)代表选择子的特权级,共有4个特权级(0级、1级、2级、3级),0级最高,CPU只能访问同一特权级或级别较低特权级的段。

在这里插入图片描述

图7.2-1 段选择子格式

例如:给出逻辑地址:0x21:0x12345678,需要将其转换为线性地址。段选择子0x21=0000000000100 0 01b,它代表的意思是:段选择子的索引为4,选择GDT中的第4个描述符;最右边的01b代表特权级RPL为1级。段内偏移为0x12345678,若此时GDT第四个描述符中描述的段基址(Base)为0x11111111,则线性地址=0x11111111+0x12345678=0x23456789。

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

线性地址(可以被理解为虚拟空间)由虚拟页号VPN和虚拟页偏移VPO两部分组成。这里我们假设VPO有p位,虚拟地址一共n位。

线性地址到物理地址的变换需要以下步骤:首先从页表基址寄存器PTBR中,得到hello进程的页表首地址。同时根据线性地址前n-p位,即VPN,在页表中找到与之对应的索引项(对应PTE的位置=VPN*PTE的大小+页表首地址),然后看是否有效,若有效则得到物理页号PPN;无效又可分为未缓存和未分配两种情况,前者需要将对应虚拟页缓存进物理页,会引发缺页故障,后者则直接报错。最后将PPN与VPO相组合,就可以得到物理地址。

在这里插入图片描述

图7.3-1 使用页表的地址翻译(教材图)

下面用书上的图进一步说明页面命中和缺页两种情况。

在这里插入图片描述

图7.3-2 两种情况(教材图)

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

7.4.1 TLB

为进一步节省时间开销,MMU中包括了一个关于PTE的小的缓存,成为快表(Translation Lookaside Buffer)。其中每一行存储着一个由单个PTE组成的块,TLB的组成形式与高速缓存很相近。用于组选择和行匹配的索引和标记字段都是从虚拟地址的虚拟页号中提取出来的。

在这里插入图片描述

图7.4.1-1 虚拟地址中用以访问TLB的组成部分(教材图)

它的具体操作方法同样用书上的图加以说明。若TLB不命中,MMU会从L1高速缓存中取出相对应的PTE,更新TLB,此过程可能会导致一个原来存在的条目被驱逐。去到对应的PTE后,将PPN和VPO相组合得到物理地址。

在这里插入图片描述

图7.4.1-2 TLB命中和不命中操作图(教材图)

7.4.2四级页表

首先来介绍多级页表的概念。如果只用一级页表,应用所引用的只是虚拟地址内存中的很小一部分,页表所占用的内存却很大,这就导致了内存空间的浪费。为解决这一问题,多级页表被提了出来。32位系统一般为两级页表,64位系统一般为四级页表。注意在多级页表中,只有最后一级页表中存储的是PPN,其余页表中存储的都是下一级页表的起始地址。第一级页表的地址同样在PTBR中存储。下面以书上的图进一步进行说明。

在这里插入图片描述

图7.4.2-1 多级页表(教材图)

7.4.3 Core i7地址翻译情况

此过程将多级页表和TLB相结合,如下图所示。其中CR3是一个寄存器,存储第一级页表的地址。

在这里插入图片描述

图Core i7地址翻译的概况(教材图)

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

上文我们分析了从虚拟地址到物理地址的变换。在得到物理地址后,将其送给L1缓存,从物理地址中取出标记、组索引信息进行匹配。如果对应组中有一路的标记与物理地址相匹配,且该路的有效位为1,则Cache命中,根据块偏移取出一定数量的数据返回给CPU。如果Cache不命中,继续去下一级存储中查询。若在下一级查找成功,则将数据加载入上一级缓存(此时还要看上一级缓存有没有满,若上一级缓存已经存满,则要驱逐某个路),进一步传递给CPU。

在这里插入图片描述

图7.5-1 高速缓存读取(PPT)

7.6 hello进程fork时的内存映射

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

在这里插入图片描述

图7.6-1 写时复制机制(教材图)

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序替代了当前程序。正如书上讲述的,加载并运行hello需要以下几个步骤:

(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。

(2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。

(3)映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

在这里插入图片描述

图7.7-1 加载器是如何映射用户地址空间的区域的

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

当根据传入MMU的虚拟地址在页表中找不到对应项时就会发生缺页故障。下图为其处理机制。发生缺页故障会产生一个异常信号,内核会把控制转移给缺页异常处理程序。若物理内存中有空余位置,则直接将对应虚拟页复制到物理内存中任意空闲的位置,并修改页表中对应的项目;若物理内存中没有空余的位置,则使用相应的驱逐机制,驱逐一个页,然后将对应虚拟页加载进物理内存。

在这里插入图片描述

图7.8-1 缺页故障与处理机制

下面再举一个书上的例子进一步说明。当要从内存中读取VP3时,从有效位判断VP3未被缓存,触发缺页故障。这时缺页故障会调用缺页异常处理程序选择一个牺牲页,在此例中就是VP4。如果VP4被修改过,则内核就会将它复制回磁盘。之后修改VP4的页表条目,将有效位变为0。接下来内核从磁盘复制VP3到内存中的PP3,更新PTE3。异常处理程序返回后重新启动导致缺页的指令,进行正常处理。

在这里插入图片描述

图7.8-2 缺页前

在这里插入图片描述

图7.8-3 缺页处理后

7.9动态存储分配管理

7.9.1动态内存分配基本概念

当C程序运行时额外需要虚拟内存时,使用动态内存分配器来分配。动态内存分配器维护着一个进程中称为堆(heap)的虚拟内存区域。堆是一个请求二进制零的区域,它紧接在未初始化的数据区域(.bss)后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

在这里插入图片描述

图7.9.1-1 堆(教材图)

分配器有两种基本风格,两种风格都要求应用显式的分配块,它们的不同之处在于哪个实体来负责释放已分配的块。

(1)显式分配器,要求应用显式地释放任何已分配的块,例如C语言提供一种叫做malloc程序包的显示分配器。

(2)隐式分配器,要求分配器检测一个已分配块何时不再被程序所使用,其又被称为垃圾收集器。

7.9.2隐式空闲链表

大多数分配器将一些基本信息(块大小,已分配/空闲等)存入堆块中,格式如下。

在这里插入图片描述

图7.9.2-1 一个简单的堆块格式(教材图)

隐式空闲链表的结构如下。因为空闲块是通过头部中的大小字段隐含的链接起来的。下图中的阴影部分为已分配块,没有阴影的部分是空闲块,头部标记为(块大小/已分配位)。

在这里插入图片描述

图7.9.2-2 用隐式空间链表组织堆(教材图)

7.9.3显示空闲链表

由于隐式空闲链表上块分配与堆块的总数呈线性关系,所以对于通用的分配器,隐式空闲链表是不合适的,一种更好的方法是将空闲块组织为某种形式的显式数据结构,例如堆可以组织成一个双向空闲链表,如下图所示。

在这里插入图片描述

图7.9.3-1 使用双向链表的堆块格式(教材图)

7.10本章小结

本章结合hello,说明了逻辑地址、线性地址、虚拟地址、物理地址的概念。然后针对Intel 32位叙述了在段式管理之下,逻辑地址到线性地址(虚拟地址)的变换是如何完成的。之后叙述了Linux系统在页式管理之下,线性地址到物理地址的变换是如何完成的。还分析了介绍了TLB和多级页表的概念及提出意义,并说明了TLB与四级页表支持下的VA到PA的变换。然后又介绍了三级Cache支持下的物理内存访问的流程,并以hello进程为例,分析了fork与execve时的内存映射。本章还通过引用教材中的例子说明了缺页故障与缺页中断的处理。最后,分析了动态存储分配管理。从动态内存管理的基本方法与动态内存管理的策略两个方面对动态内存管理进行介绍。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:一个Linux文件就是一个m字节的序列,所有的IO设备都被模型化为文件,甚至内核也被映射为文件。

设备管理:这种将设备优雅地映射为文件的方式允许Linux内核引出一个简单、低级的应用接口,其被称为Unix IO。

8.2 简述Unix IO接口及其函数

8.2.1 Unix IO接口

(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

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

(3)读写文件:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k。

(4)关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。

8.2.2函数

Unix中大多数文件的操作只需要用到五个函数open、read、write、lseek、和close(与课上讲的多了lseek),下面分别进行讲述。

(1)int open(char* filename,int flags,mode_t mode),进程调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

(2)ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

(3)ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。

(4)off_t lseek(int fildes,off_t offset ,int whence),应用程序通过lseek函数能够显示地修改当前文件的位置。参数fildes为已打开的文件描述词,参数offset为根据参数whence来移动读写位置的位移数。

(5)int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。

8.3 printf的实现分析

先来看printf的函数体,如下图所示。

printf函数的参数为可变形参(出现了…),当传递参数不确定时就可以用这种形式来表示。接下来看蓝框框住的内容,va_list使用char *定义的,因此这句话的意思就是让arg指向fmt后第一个参数的位置。然后看下面一句,调用了vsprintf函数,下面结合图8.3-2(这里的代码只显示了对十六进制的格式化)分析vsprintf函数。若格式化串当前指针处不是%,则直接复制到buf中,反之则根据%后的内容进行格式化(不同的字母代表不同的含义,d代表十进制数字等等)。返回值为输出字符串的长度(printf函数体中i的值)。返回printf函数中后,再调用write函数进行屏幕输出。

在这里插入图片描述

图8.3-1 printf函数体(网站截图)

在这里插入图片描述

图8.3-2 vsprintf函数(网站截图)

在来详细看看write的反汇编代码,如下所示。在write中最后进行了系统调用,显示格式化了的字符串。

在这里插入图片描述

图8.3-3 wirte函数反汇编代码(网站截图)

在这里插入图片描述

图8.3-4 syscall(网站截图)

接下来,系统已经确定了所要显示在屏幕上的符号。根据每个符号所对应的ASCII码,系统会从字模库中提取出每个符号的VRAM信息(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。进入getchar函数之后,进程会进入阻塞状态,等待外界的输入。系统开始检测键盘的输入。此时如果按下一个键,就会产生一个异步中断,这个中断会使系统回到当前的getchar进程,然后根据按下的按键,转化成对应的ASCII码,保存到系统的键盘缓冲区。

getchar调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar是读入函数的一种。它从标准输入里读取下一个字符,相当于getc(stdin)。getchar会将这个字符串保存在一个静态的缓冲区中,并返回其第一个字符。在下次调用getchar时,将直接从静态的缓冲区中取出字符并返回,而不是通过read再次进行读取,直到静态缓冲区为空,才再调用read进行读取。

8.5本章小结

本章介绍了linux系统下的IO设备的管理方法,讨论了Linux系统中Unix I/O的形式以及实现的模式函数。最后,介绍了printf和getchar两个函数的底层实现。

结论

程序员向计算机输入一行行C语言代码,并将其保存为一个.c文件——hello.c,hello的一生由此开始。接下来预处理器,编译器,汇编器和连接器轮番上阵,hello.c摇身一变,变成了一个可执行文件hello,可以被加载器加载入内存执行。

之后,程序员在shell中输入命令“./hello 2022111385 乔思远 1”,shell在判断其不为内部指令后,通过fork函数创建新进程,并为其分配虚拟内存,在新进程中调用execve函数将hello程序加载入内存,操作系统为其分配时间片,使得它可以被CPU执行。

执行时,CPU一条条的从内存取指令,MMU,TLB,3级Cache等设备忙的不可开交;IO设备,异常和信号处理程序时刻关注着程序的一举一动。hello中printf函数底层触发陷阱,执行write函数,在屏幕上输出相关内容。

最后,程序运行结束,shell对hello进程进行回收,内核把它从系统中清除。这样,hello就结束了它的一生。

在了解了hello的一生后,我收获了很多,对于计算机系统设计与实现有了更深切的感悟。hello之前为我打开了编程的大门,现如今其又为我打开了计算机底层系统的大门。计算机仍有很多我未知的领域,这些谜团就让我在以后的计算机学习道路中来解答吧。

附件

hello.c:源代码文件

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

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

hello.o:汇编后的可重定位目标文件

hello:可执行文件

hello.asm:hello的反汇编代码

hello.o.asm:hello.o的反汇编代码

hello.elf:hello的ELF信息

hello.o.elf: hello.o的ELF信息

参考文献

  1. 《深入理解计算机系统》

  2. 课程PPT

  3. __printf_chk函数:

__printf_chk (linuxbase.org)

  1. atoi函数和strtol函数:

atoi,atol,strtod,strtol,strtoul详解_strtol atol-CSDN博客

  1. sleep函数

关于Linux 中sleep()函数说明_linux sleep 函数的单位-CSDN博客

  1. readelf命令

Linux命令学习手册-readelf - 知乎 (zhihu.com)

  1. hexdump命令

Linux命令学习总结:hexdump - 潇湘隐者 - 博客园 (cnblogs.com)

  1. pstree命令

学习一个Linux命令-pstree - 知乎 (zhihu.com)

  1. Linux下ELF解读

ELF格式解读-(1) elf头部与节头_elf文件头-CSDN博客

ELF文件结构描述 - yooooooo - 博客园 (cnblogs.com)

ELF 文件解析 2-节 - 知乎 (zhihu.com)

  1. EDB带参数使用

EDB的安装和如何带参数运行程序-CSDN博客

  1. 逻辑地址(分段管理)

Linux 线性地址,逻辑地址和虚拟地址的关系? - 知乎 (zhihu.com)

【构建操作系统】全局描述符表GDT - 知乎 (zhihu.com)

  1. Unix接口IO及其函数

unix环境下的文件操作的一些函数open()、close()、read()、write()、dup()、fsync()sync()函数_read、write和fsync-CSDN博客

  1. printf函数

湘隐者 - 博客园 (cnblogs.com)](https://www.cnblogs.com/kerrycode/p/5077687.html)

  1. pstree命令

学习一个Linux命令-pstree - 知乎 (zhihu.com)

  1. Linux下ELF解读

ELF格式解读-(1) elf头部与节头_elf文件头-CSDN博客

ELF文件结构描述 - yooooooo - 博客园 (cnblogs.com)

ELF 文件解析 2-节 - 知乎 (zhihu.com)

  1. EDB带参数使用

EDB的安装和如何带参数运行程序-CSDN博客

  1. 逻辑地址(分段管理)

Linux 线性地址,逻辑地址和虚拟地址的关系? - 知乎 (zhihu.com)

【构建操作系统】全局描述符表GDT - 知乎 (zhihu.com)

  1. Unix接口IO及其函数

unix环境下的文件操作的一些函数open()、close()、read()、write()、dup()、fsync()sync()函数_read、write和fsync-CSDN博客

  1. printf函数

[转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蝴蝶飞过废墟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值