哈工大计算机系统大作业论文

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业           网络空间安全            

学     号            2022110523           

班     级           2203901            

学       生           王悦雷          

指 导 教 师            史先俊           

计算机科学与技术学院

2024年5月

摘  要

本文通过对一个hello.c的简单的 C语言文件在linux系统下从预处理到编译、汇编、链接直到处理为一个可执行文件的过程的分析,以及对hello程序的加载,运行,异常处理,终止,回收的过程,从而了解了hello.c的一生。本论文以hello.c为主要研究对象,通过《深入理解计算机系统》以及老师的讲授,在ubuntu系统上对hello.c的“一生”进行研究,将理论和实践运用相结合。

关键词:计算机系统;linux系统;程序的生命周期。                           

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

目  录

第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的P2P,020的整个过程。

P2P: Program to Process当我们编写好一个hello.c的C语言程序,它将作为程序一个文本存储。运行这个程序时,由编译器驱动程序启动,读取hello.c文件,然后进行预处理:修改了源程序(文本),得到另一个C语言程序,hello.i文件;之后由编译器对hello.i进行编译:得到一个汇编语言程序(文本),即hello.s。之后将汇编程序交由汇编器进行汇编:得到一系列机器语言指令,并将这些机器语言指令打包成一种“可重定位目标程序”,并将其存入到hello.o(二进制)文件中。最后由链接器进行链接,需要对prinf.o等程序进行链接:结果就得到了可执行目标文件:hello。接下来计算机就可以运行这个hello文件了。之后在计算机的Bash(shell)中,OS会为hello创建子进程(fork),这样,在计算机系统中,hello就有了自己独一无二的进程(Process),在这个进程中hello便可以运行。

020: from Zero-0 to Zero-0 程序并不是一开始就在内存空间中的,也就是一开始为0。OS为hello fork一个子进程,然后在execve执行hello程序,OS会为他开辟一个块虚拟内存,并将程序加载到虚拟内存映射到的物理内存中。当程序执行完,OS回收这一程序,同时为该程序的开辟的内存空间也会被回收,此时又变为0。

1.2 环境与工具

  1. 硬件信息:

处理器:13th Gen Intel® Core(TM) i9-13900HX(32 CPUs)

内存:32GB RAM

磁盘:1T HDD + 1T SSD

  1. 软件:

Window11 64位

Ubuntu 20.04 LTS 64位

  1. 调试工具:

Visual Studio 2016 64-bit;

gedit,gcc,notepad++,readelf, objdump, hexedit, edb

1.3 中间结果

以下是对hello从hello.c到可执行目标程序过程中的各种程序以及调试,查看汇编代码等用到的各种程序。

文件名

功能

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello2.elf

由hello可执行文件生成的.elf文件

hello2.asm

反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

       本章主要从整体上介绍了hello的P2P,020的具体含义,同时简要介绍了hello从编辑好代码到运行的过程中经历的不同阶段,产生的不同文件。同时介绍了论文研究用到的硬件环境,软件以及开发工具和中间结果等。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

  • 预处理的概念

预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程。例如,hello.c文件6、7行中的#include 命令会告诉预处理器读取系统头文件stdio.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。

  • 预处理的作用

预处理过程将#include后继的头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理。预处理过程中并未直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换。简单来说,预处理是一个文本插入与替换的过程,生成的hello.i文件仍然是文本文件。

2.2在Ubuntu下预处理的命令

在Ubuntu下使用gcc指令:gcc -E hello.c -o hello.i(使用cpp也可以,cpp只有预处理功能),截图如下:

图 1预处理命令及结果

2.3 Hello的预处理结果解析

在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3110行,行数比起hello.c文件大幅增加。其中, hello.c中的main函数相关代码在hello.i程序中对应着3048行到3061行。

图 2 hello.i文件内容

在main函数内代码出现之前是大段的头文件 stdio.h unistd.h stdlib.h 的依次展开。展开的具体流程概述如下(以stdio.h为例):CPP先删除指令#include <stdio.h>,并到Ubuntu系统的默认的环境变量中寻找 stdio.h,最终打开路径/usr/include/stdio.h下的stdio.h文件。若stdio.h文件中使用了#define语句,则按照上述流程继续递归地展开,直到所有#define语句都被解释替换掉为止。除此之外,CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。

2.4 本章小结

本章主要介绍了预处理的概念及作用、并结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序对预处理结果进行了解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用       

  • 编译的概念

编译是指C编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码的过程。通过编译过程,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s,在hello.s中,以文本的形式描述了一条条低级机器语言指令。

  • 编译的作用

将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备。

3.2 在Ubuntu下编译的命令

使用gcc命令:gcc -S hello.i -o hello.s

截图如下:

图 3 编译命令及结果

3.3 Hello的编译结果解析

Hello.s文件内容(部分)如下:

图 4 hello.s文件内容

3.3.1 数据

常量:

  1. 字符串常量:如下图所示,分别对应:

图 5 c程序中的字符串

两者均为字符串常量,储存在.text数据段中。\XXX为UTF-8编码,一个汉字对应三个字节。

图 6 hello.s中对应的字符串语句

  1. 数字常量:

C语言程序中可以看到:代码:“argc1=5”在汇编代码中5对应立即数$5

图 7 c程序中常量部分

图 8 hello.s中的常量

  1. 局部变量:

局部变量运行时存放在用户栈中,并不占用文件实际节的空间。

从下图可以看到:为int i在栈中分配了四个字节的空间

图 9 hello.s中为局部变量分配空间

                      对于数组的局部变量也一样在栈中分配空间。

3.3.2 赋值

从源c程序中可以看到在循环中使用了i=0这个赋值语句

图 10 i=0赋值语句

对应在hello.s中的部分:

图 11 hello.s中赋值语句

3.3.3 类型转换

本程序中没有涉及到使用sizeof运算符

3.3.4 sizeof

本程序中没有涉及到使用sizeof运算符

3.3.5 算术运算

 以下是汇编语言中算术运算表:

可以看到有加载有效地址,自增,自减,加减乘除等操作。

指令

效果

leaq s,d

d=&s

inc d

d+=1

dec d

d-=1

neg d

d=-d

add s,d

d=d+s

sub s,d

d=d-s

imulq s

r[%rdx]:r[%rax]=s*r[%rax](有符号)

mulq s

r[%rdx]:r[%rax]=s*r[%rax](无符号)

idivq s

r[%rdx]=r[%rdx]:r[%rax] mod s(有符号) r[%rax]=r[%rdx]:r[%rax] div s

divq s

r[%rdx]=r[%rdx]:r[%rax] mod s(无符号) r[%rax]=r[%rdx]:r[%rax] div s

在代码中有开辟栈帧(数组),修改地址偏移量来读值,i++等操作:

图 12 c程序中使用算数运算的部分

由此可以在汇编代码中找到对应的部分:

图 13 汇编代码中涉及到的算术部分

3.3.6 逻辑/位操作

 本程序中没有涉及到逻辑/位运算(>>,<<,|)等。

3.3.7 关系操作

 C程序中涉及到的关系操作如下图所示:

 可以看到有不等于和小于的判断

图 14 c程序涉及关系操作的部分

对应汇编代码如下:

第一个:判断 argc!=5,比较argc与5的大小,如果等于那么跳转到.L2否则继续执行下面的指令。

图 15 argc的关系处理

     第二个:判断i和10的大小,这里用的是和9比,如果小于等于就跳转

图 16 汇编中i的关系操作

3.3.8 数组/指针/结构操作

数组,指针,结构的操作包括:对数组的引用(a[i],a[j]);指针包括取地址和解引用等(*p,&p);结构操作包括对结构体和联合(union)的使用等。

在c程序中包括:如下对argv的操作都是数组操作,通过对数组下标的引用来调用对应的元素。

图 17 c程序中的数组例子

下面对应汇编代码进行解释:

第一条语句中将保存栈顶位置的寄存器rsp的值减32,然后之后可以看到第二条语句将第二个参数%rsi(数组argv)放在了-32(%rbp)的位置。

图 18 数组操作1

接下来是printf中要打印出argv数组中对应不同的元素,0,1,2,所以要在栈中拿出对应的数据,以下对应的汇编代码展示了拿取三个值的过程:栈顶加上不同的偏移量,得到对应元素的首地址,然后存到一个寄存器中,偏移量为8是因为数组元素是一个指针类型,对应8个字节,偏移量为8对应1号,偏移量为16对用2号,以此类推。

图 19 数组操作2

之后对argv[4]的操作大同小异,不做过多赘述。

3.3.9 控制转移

 控制转移包括:if/else switch for while  do/while  ?:       continue  break

 C程序中涉及到for循环的部分,其实for循环对应的汇编代码和while、do while 的实现方式比较类似。

图 20 c程序中的for循环部分

     下面是根据汇编代码的解释:

     首先可以看到L2中,将-4(%rbp)的值设置为0,其实就是设置i=0,后面直接跳转到L3部分,在L3部分中,首先比较i与9,如果小于等于则跳转到L4(条件跳转,条件:ls:less equal),在L4中其实就是循环体,然后顺着循环体不断进行,自然到达L3部分时,继续进行判断,如果小于等于,重复以上过程,如果大于,那么执行L3剩余的代码,需要注意的是i++对应的操作就是L4中的最后一行操作。

图 21 for循环的汇编代码解释

  3.3.10 函数操作

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

  下面对这些概念进行解释:

      参数传递:函数中的参数1-6保存在对应的寄存器中:rdi,rsi,rdx,rcx,r8,r9等,更多的参数保存在栈中。

      函数调用:主要用call指令实现,call指令主要做两件事情:将本条指令的下一个指令保存在栈中以作为函数返回的地址,然后将pc(存储下一条指令的地址)更新为call后面的地址,也就是跳转到对应函数的地址。

      局部变量:保存在用户栈中或者寄存器中,为了保证调用的函数不会覆盖“原函数”用的寄存器的值,x86-64设置了两种寄存器:

  • 被调用者寄存器:rbp,rbx和r12~r15。当P调用Q时,Q必须“保存”这些寄存器的值——要么不去使用,要么压入栈中然后返回前恢复。
  • 调用者保存寄存器:剩下的所有的寄存器除了rsp都属于调用者保存寄存器,如果想要确保调用过程中这些值不被破坏那么调用者必须主动保存它们。

      函数返回:使用ret将pc设置为栈中之前call存放的返回地址,返回值放在rax寄存器中。

图 22 汇编代码中对应call指令的例子

图 23 汇编代码中对用ret指令的例子

3.4 本章小结

本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。同时,本章以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,通过从指令用法到c语言程序中的例子,再到汇编代码中具体的实现流程和讲解,验证了大部分数据、操作在汇编代码中的实现。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

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

4.1.2汇编的作用

    汇编器(as)将.s汇编程序翻译成机器语言并将这些指令打包成可重定目标程序的格式存放在.o目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。汇编过程从汇编程序得到一个可重定位目标文件,以便后续进行链接。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

在终端输入:gcc -c hello.s -o hello.o

图 24 linux下执行汇编命令结果

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

在终端执行命令:readelf -a hello.o  > hello.elf得到hello程序的elf格式:

图 25 得到hello.elf

以下是对hello.elf的分析:

典型的可重定位目标文件的elf文件的格式如下:

图 26 典型的ELF可重定位目标文件

  1. ELF头:以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。具体如下图所示,字的大小:64,字节顺序:小端.ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息,其中包括ELF头的大小,目标文件的类型,机器类型,节头部表的文件偏移,以及节头部表中条目的大小和数量。

在图中可以看到:文件类型:可重定位目标文件,系统架构:x86-64。

图 27 ELF头

不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

  1. 节头部分:

图 28 ELF文件中节头

  1. 重定位节:.rela.data节

一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

具体包含以下信息:

偏移量

代表需要进行重定向的代码在.text或.data节中的偏移位置

信息

包括symbol和type两部分,其中symbol占前半部分,type占后半部分,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型

类型

重定位到的目标的类型

加数

计算重定位位置的辅助信息

下面几条重定位符号是对puts,exit,printf,atoi,sleep,getchar等函数进行重定声明。

图 29 .rela.text节

  1. 重定位节.rela.eh_frame

图 30 rela.eh_frame节

  1. 符号表:Symbol table

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

图 31 Symbol table节

4.4 Hello.o的结果解析

反汇编得到如下:

图 32 hello.o得到的反汇编

由上图可知:

左侧为机器语言反汇编得到的结果,右为汇编程序。

可以看到机器语言程序是由机器指令组成的,反汇编中每一行的1到5个字节不等的16进制数表示就是一条机器指令,对应汇编语言中的一行。机器指令可以是变长的,常用的,操作数少的指令字节数少;不常用的,操作数多的指令字节数多。

机器语言中所有的操作数都是16进制形式的,而汇编语言里操作数可以有10进制形式的。

机器语言中call后面跟的是该函数的地址,而汇编语言中call后面跟的是函数名。

机器语言中跳转的目标是地址,而汇编语言中跳转的目标是标号。

以下进行具体分析和对照:

  1. 分支转移:

在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。

如下图je部分例子。

图 33 分支转移例子

  1. 函数调用:

在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全0(此时,目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。

图 34 函数调用

4.5 本章小结

本章介绍了汇编的概念与作用,同时在linux系统下对c程序进行汇编操作,得到hello.o可重定向目标文件,之后对其ELF格式进行具体的分析,同时对Hello.o的结果进行解析,说明了机器语言的构成以及与汇编语言的映射关系,尤其说明了在分支转移和函数调用的不同。

(第41分)

5章 链接

5.1 链接的概念与作用

  1. 链接的概念

链接是指通过链接器(Linker),将程序编码与数据块收集并整理成为一个单一文件,生成完全链接的可执行的目标文件(windows系统下为.exe文件,Linux系统下一般省略后缀名)的过程。

  1. 链接的作用

提供了一种模块化的方式,可以将程序编写为一个较小的源文件的集合,且实现了分开编译更改源文件,从而减少整体文件的复杂度与大小,增加容错性,也方便对某一模块进行针对性修改。

注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

正常直接链接时c语言会采用动态链接的方法,因此此处使用静态链接的命令:

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

路径有误,应修改为如图中命令:

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

结果如下:

图 35 执行ld链接

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

运行图中命令(readelf -a hello > hello.elf)可以得到hello程序的ELF格式文件:

图 36 得到hello文件的ELF文件

接下来对ELF文件进行分析:

  1. ELF头:

hello.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以 描述了生成该文件的系统的字的大小和字节顺序的16字节序列 Magic 开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与hello.elf相比较,hello.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。

图 37 ELF头部分

  1. 节头:

hello.elf中的节头包含了文件中出现的各个节的语义,包括节的类型、位置、偏移量和大小等信息。与可重定向目标文件的hello.elf相比,其在链接之后的内容更加丰富详细。

图 38 节头部分

  1. 程序头:

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。描述了可执行文件的连续的片被映射到连续的内存段的映射关系,每一个表提供了各段在虚拟地址空间大小和物理地址,标志,访问权限和对其方式,可以一次读出各段的起始地址。

图 39 程序头

  1. Dynamic section

图 40 Dynamic section部分

  1. Symble tab部分

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明

图 41 symble tab部分

  • 各段的起始地址如上文所说,可以在程序头部表(即第三点)中读出。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。  

如下图所示,程序加载在0x400000至0x401000处,在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。根据edb查看的结果,在地址空间0x400000~0x400fff中存放着与地址空间0x400000~0x401000相同的程序,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。

图 42 使用edb查看虚拟地址空间

5.5 链接的重定位过程分析

以下格式自行编排,编辑时删除

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

运行 objdump -d -r hello后得到如下结果:

图 43 查看可执行文件的elf

下面进行具体分析:

  • 链接之后文件量增加,函数部分增加:因为链接之后会从静态库中寻找原程序引用的函数等如printf,getchar等,然后提取对应的.o文件(如printf.o,getchar.o)然后一起链接为一个可执行文件。

图 44 链接后添加的函数

  • 链接之后(重定位)每个函数有一个具体的地址而不是都是从0x000000开始的地址。

图 45 函数的地址改变

  • Call指令与函数调用的变化,如下图所示,call指令调用函数的时候,call之后的字节代码被链接器直接修改为目标地址和下一条指令的地址之差,指向响应的代码段,从而得到完整的反汇编代码。

图 46 call指令的变化

  • 跳转指令的变化。在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。

图 47 跳转指令

5.6 hello的执行流程

使用edb单步调试运行程序,观察其调用的函数,可以发现调用main之前首先要进行初始化,调用了_init函数,此后动态链接的重定位工作已经完成,可以看到这个函数的调用之后是一系列在这个程序中用到的库函数(printf,exit,atoi)等,这些函数在代码段并不占用实际的空间,只是起到一个占位的作用。之后调用了_start,这个就是起始的地址,准备开始执行main函数的内容,main函数内部所调用的函数在第三种中已经进行分析,这里不做过多赘述,执行完main函数之后会执行__libc_csu_init,__libc_csu_fini,_fini,最终结束程序。以下是各个函数的图及相应的地址。

图 48 函数及地址

5.7 Hello的动态链接分析

   (以下格式自行编排,编辑时删除

分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。

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

.got与.plt节保存着全局偏移量表GOT,其内容从地址0x601000开始。通过edb查看,在dl_init调用前,其内容如下:

图 49 调用前的情况

在调用后,其内容变为:

图 50 调用之后的情况

比较可以得知,0x601008~0x601017之间的内容,对应着全局偏移量表GOT[1]和GOT[2]的内容发生了变化。GOT[1]保存的是指向已经加载的共享库的链表地址。GOT[2]是动态链接器在ld-linux.so模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表PLT和全局偏移量表GOT进行动态链接。

5.8 本章小结

本章主要介绍了链接的概念和作用,得到并分析了可执行文件hello的elf文件,然后与链接之前的可重定向目标文件hello.o的elf格式文件进行对比分析;之后又对两个文件对应的反汇编文件进行对比分析,从而对链接的主要的两个过程:符号解析和重定位进行了分析和理解。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

  • 进程的概念

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

  • 进程的作用

给一个应用程序提供两个关键的抽象:

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

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

Shell 的作用:

Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。

Shell的处理流程大致如下:

  1. 从Shell终端读入输入的命令。
  2. 切分输入字符串,获得并识别所有的参数
  3. 若输入参数为内置命令,则立即执行
  4. 若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
  5. 若输入参数非法,则返回错误信息
  6. 处理完当前参数后继续处理下一参数,直到处理完毕

6.3 Hello的fork进程创建过程

打开终端(shell)然后输入命令:./hello 2022110523 王悦雷 18504996803 3

fork进程的创建过程如下:首先,带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。

程序运行结果如下:

图 51 带参数执行结果

6.4 Hello的execve过程

调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,例如打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

在操作系统中,每个时刻通常会有很多程序在运行,但是我们通常会认为每个进程都独立占用CPU内存以及一些其他资源,如果我们进行单步调试可以发现在执行一系列的PC中的值,这个PC值的序列就是逻辑控制流,事实上,多个程序在计算机内部执行时,采用并行的方式,他们的执行是交错的,如下图所示,进程是轮流使用处理器的(排除多核处理器),在同一个处理器中,每个进程执行他流的部分后暂时挂起,然后其他进程开始使用。

对于用户态和核心态的转换:在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程。

图 52 逻辑控制流

6.6 hello的异常与信号处理

hello程序执行过程中出现的异常可能有中断、陷阱、故障、终止等

图 53 异常类型等

  1. 程序正常运行:如下图所示,程序正常运行的时候打印十次对应内容后等待指令,按下回车结束运行。

图 54 程序正常运行的情况

  1. 程序运行时不停乱按:如下图所示,可以看到不停乱按不会对程序的正常运行造成影响

图 55 不停乱按的情况

  1. 程序运行时按回车:如下图所示,可以看到按下回车时只是会多加几行,对程序运行没有影响

图 56 按下回车的情况

  1. 按下Ctrl+C的情况:可以看到按下Ctrl+C后,程序中断,Shell进程收到SIGINT信号,Shell结束并回收hello进程。

图 57 Ctrl+C的情况

  1. 按下Ctrl+Z的情况:按下Ctrl + ZShell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

图 58 按下Ctrl+Z的情况

对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。如下图所示:

图 59 查看挂起情况

在Shell中输入pstree命令,可以将所有进程以树状图显示

图 60 树状图显示进程(部分)

输入kill命令,则可以杀死指定(进程组的)进程:

图 61 使用kill杀死进程

输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。

图 62 挂起后继续运行

6.7本章小结

本章介绍了进程的概念与作用,以及Shell-bash的基本概念。针对进程,在这一章中根据hello可执行文件的具体示例研究了fork,execve函数的原理与执行过程,并给出了hello带参执行情况下各种异常与信号处理的结果。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

  1. 逻辑地址

逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。

  1. 线性地址

逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。

  1. 虚拟地址

根据CSAPP教材,虚拟地址即为上述线性地址。

  1. 物理地址

CPU通过地址总线的寻址,找到真实的物理内存对应地址。

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

Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。

在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符的结构如下:

图 63 段选择符

其包含三部分:索引,TI,RPL

索引:用来确定当前使用的段描述符在描述符表中的位置;

TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);

RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;

通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。

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

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

通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。

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

若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。

若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。

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

针对Intel Core i7 CPU研究VA到PA的变换。

Intel Core i7 CPU的基本参数如下:

  1. 虚拟地址空间48位(n=48)
  2. 物理地址空间52位(m=52)
  3. TLB四路十六组相连
  4. L1,L2,L3块大小为64字节
  5. L1,L2八路组相连
  6. L3十六路组相连
  7. 页表大小4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8字节

由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表大小4KB,PTE大小8字节,则单个页表有512个页表条目,需要9位二进制进行索引,而四级页表则需要36位二进制进行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32位。

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

如图所示, CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。多级页表的工作原理展示如下:

图 66 多级页面的工作原理

若查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

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

因为三级Cache的工作原理基本相同,所以在这里以L1 Cache为例,介绍三级Cache支持下的物理内存访问。

L1 Cache的基本参数如下:

  1. 8路64组相连
  2. 块大小64字节

由L1 Cache的基本参数,可以分析知:

块大小64字节→需要6位二进制索引→块偏移6位

共64组→需要6位二进制索引→组索引6位

余下标记位→需要PPN+PPO-6-6=40位

故L1 Cache可被划分如下(从左到右):

CT(40bit)CI(6bit)CO(6bit)

在7.4中我们已经由虚拟地址VA转换得到了物理地址PA,首先使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出相应的数据后返回。

若没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中请求数据(请求顺序为L2 Cache→L3 Cache→主存,若仍不命中才继续向下一级请求)。查询到数据之后,需要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用LFU策略进行替换。

7.6 hello进程fork时的内存映射

当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数加载并运行hello需要以下几个步骤:

  1. 删除已存在的用户区域

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

  1. 映射私有区域

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

  1. 映射共享区域

若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。

  1. 设置程序计数器

最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

发生一个缺页异常后,控制会转移到内核的缺页处理程序。判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。

若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。

7.9动态存储分配管理

动态内存管理的基本方法与策略介绍如下:

动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。

具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。

显式分配器:要求应用显式地释放任何已分配的块。

隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

下面介绍动态存储分配管理中较为重要的概念:

  1. 隐式链表

堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。对于隐式链表,其结构如下:

图 67 隐式链表的解构

  1. 显式链表

在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。

显式链表的结构如下:

图 68 显式链表的结构

  1. 带边界标记的合并

采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。

  1. 分离存储

维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。

7.10本章小结

本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。

8.2 简述Unix IO接口及其函数

  1. Unix I/O接口:
  1. 打开文件

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

  1. 改变当前的文件位置

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

  1. 读写文件

一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

  1. 关闭文件

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

  1. Unix I/O函数:
  1. int open(char* filename,int flags,mode_t mode)

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

  1. int close(fd)

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

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

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

  1. ssize_t wirte(int fd,const void *buf,size_t n)

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

8.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

  1. 先找到printf的函数定义:

图 69 printf函数定义

  1. 先看参数部分:“…”是可变形参的一种写法,如果传递的参数的个数不确定时,那就使用这种方法来表示
  2. va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。
  3. 再进一步查看windows系统下的vsprintf函数体:

图 70 vsprintf函数体

  1. 则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。
  2. 再进一步对write进行追踪:

图 71 write部分

这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:

图 72 syscall的实现

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

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

8.5本章小结

本章主要介绍了linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的底层实现有了基本了解。

(第81分)

结论

hello程序的一生经历了如下过程:

1.    预处理

将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理;

2.    编译

通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;

3.    汇编

将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中;

4.    链接

通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello;

5.    加载运行

打开Shell,在其中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;

6.    执行指令

在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;

7.    访存

内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;

8.    动态申请内存

printf 会调用malloc 向动态内存分配器申请堆中的内存;

9.    信号处理

进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作;

10.  终止并被回收

Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

文件名

功能

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello2.elf

由hello可执行文件生成的.elf文件

hello2.asm

反汇编hello可执行文件得到的反汇编文件

(因为需要对hello.o和hello都生成一次elf格式文件和.asm文件,因此在论文讲述过程中在不同的阶段只使用hello.elf和hello.asm来解释,最后的附件中使用hello.elf和hello2.elf来区分)

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

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]   Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4

[2]    Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].

https://www.cnblogs.com/pianist/p/3315801.html.

[3]   梦想之家xiao_chen.ELF文件头更详细结构[EB/OL].2017[2021-6-10].

https://blog.csdn.net/qq_32014215/article/details/76618649.

[4]   Florian.printf背后的故事[EB/OL].2014[2021-6-10].

https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值