csapp 程序人生 Hello’s P2P

Posted 烤地蛋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了csapp 程序人生 Hello’s P2P相关的知识,希望对你有一定的参考价值。

 

大作业

题     目  程序人生-Hellos P2P  

专       业        计算学部          

学  号             

班       级               

学       生             

指 导 教 师         

计算机科学与技术学院

2022年5月

摘  要

文章通过对hello.c程序在Linux系统下的生命周期进行追踪,逐步分析其在预处理、编译、汇编、链接生成可执行文件中的变化表现,并在Ubuntu系统下执行,观察其加载、运行、终止、回收的过程,实现了对程序翻译的系统性了解,以及对其运行机制的系统性的分析与概述。

关键词:计算机系统;编译系统;进程管理;存储                           

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献

第1章 概述

1.1 Hello简介

P2P:即From Program to Process。如下图1,hello程序从源文件hello.c开始,经过预处理器cpp的预处理,得到hello.i文件,这是一个经修改了的源程序;hello.i文件再经过编译器cc1处理,得到文本文件hello.s;hello.s再经过汇编器as处理,被翻译成机器语言指令,打包成二进制文件hello.o;再经过链接器ld,将hello.o文件和一些必要的系统目标文件组合起来,就得到了可执行目标文件hello。接着,用户在shell输入./hello即可执行该文件。Shell会通过一系列操作将代码加载到内存,并调用fork函数创建一个新的进程,再调用execve函数,将hello程序加载到该子进程中执行。

                                                             图1 从源文件到可执行文件

020:即From Zero to Zero。接上述过程,shell执行execve函数将程序加载到进程中,进行虚拟内存映射,将程序载入物理内存中执行,后开始执行目标代码,CPU为运行的hello分配时间片来执行逻辑控制流,运行结束后,父进程回收这个子进程,内核删除相关的数据结构,释放资源,hello归于zero。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;4GRAM;256Disk;

软件环境:Windows10 64位;Vmware 16;Ubuntu 20.04 LTS 64位;

工具:gcc;edb;as;cpp;cc1;gedit;GNU READELF等

1.3 中间结果

hello.i 预处理后得到的文件;

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

hello.o 汇编得到的可重定位目标文件;

hello 链接得到的可执行文件;

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

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

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

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

1.4 本章小结

本章是对hello程序“一生”的过程的简述,主要围绕P2P(从程序到进程)和020(从无到有到无)的过程来进行简略介绍,并给出了实验时的软硬键环境、开发调试工具等基本信息。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——预处理记号用来支持语言特性(如C/C++的宏调用)。

作用:预处理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本,会根据以字符#开头的命令来修改原始的C程序。比如,会读取头文件的相应内容,并将其直接插入到程序文本中;用实际值来替换掉“#define”定义的字符串;根据“#if”后的条件来决定需要编译的代码;此外还有#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)等的处理。

2.2在Ubuntu下预处理的命令

命令:gcc hello.c -E -o hello.i或者cpp hello.c hello.i

                                                                图2.1 预处理命令

2.3 Hello的预处理结果解析

经预处理得到了hello.i文件,使用gedit查看hello.i文件部分内容,见下图:

 

                                                                图2.2.1 hello.i部分内容

                                                                图2.2.2 hello.i部分内容

源文件的预处理删去了hello.c文件中的注释内容,引入了头文件,对宏进行了展开,得到了一个三千多行的代码,该代码仍是C程序。

2.4 本章小结

本章主要介绍了预处理的相关概念和具体作用,并结合hello.c文件的预处理进行实际的结果分析:根据以字符#开头的命令来修改原始的C程序,比如宏定义的扩展、头文件的代码引入等。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:可通过编译器(cc1)将文本文件.i翻译成.s文本文件,它包含一个汇编语言程序。

作用:它是将高级语言程序转化为机器可直接识别处理执行的的机器码的中间步骤,会进行语法检查、目标程序优化等。

3.2 在Ubuntu下编译的命令

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

图3.1 编译命令

3.3 Hello的编译结果解析

3.3.1汇编初始部分

 

图3.2.1 hello.s部分代码

图3.2.2 hello.s部分代码

文件的开头是一些声明:.file声明源文件;.test是代码段;.section.rodata是只读数据段;.align声明对指令或数据的存放地址的对齐方式;.string声明字符串;.globl声明全局变量;.type声明一个符号是函数类型还是数据类型;.size声明大小。

3.3.2数据

(1)数字常量:源代码中有将参数argc和常数4比较,4便以立即数的形式直接用于比较了;exit的1也同样以立即数的形式出现。

图3.3 数字常量

(2)局部变量:main函数里有声明一个局部变量i,它被存储在了栈中(图3.4.1);参数argc和argv也是局部变量,同样被存储在了栈里(图3.4.2)。

图3.4.1 局部变量i

图3.4.2 参数argc及argv

(3)字符串常量:源程序中的两个printf的参数都是字符串常量分别别为:"用法: Hello 学号 姓名 秒数!\\n"、"Hello %s %s\\n"。这两个字符串都存储在只读数据段中。

图3.5  符串常量

3.3.3赋值

在源代码中,for循环部分将i赋值0,对应汇编代码中的movl $0  -4(%rbp)指令(见图3.4.1),由于i为int型操作数,用movl来赋值“双字”。

3.3.4类型转换

源代码中有引用atoi函数,将argv[3]由字符串转换成了整型:

 

图3.6 类型转换

3.3.5算术操作与逻辑操作

在该代码文件中出现的算术操作有以下几种:

  1. subq $32, %rsp:对栈指针的减法操作,开辟了一个32字节的栈空间;
  2. addq $16, %rax:修改地址偏移量;
  3. addl $1, -4(%rbp):实现i++

其余整数算术操作见下图。

图3.7 整数算术操作

3.3.6 关系操作以及控制转移指令

关系操作即一些判断大于、等于、不小于之类的关系判断操作。在源代码中,有将参数argc与4比较,以及将i与8比较,其在汇编语言中的情况见下图:

图3.8 关系操作

先用CMP指令设置条件码,再结合不同的JXX控制转移指令得到比较的效果,若满足控制转移条件,则跳转到对应的跳转目标。控制转移指令一般出现在循环语句和条件语句中。

3.3.7数组

源程序的第二个参数argv[]是一个指针数组,被存在了栈中(见图3.4.2)。该数组的每个元素都是一个只想字符类型的指针,在汇编代码中被两次传递给了printf函数。

图3.9 数组

3.3.8函数操作

函数调用:假设P调用Q,Q执行后返回P,这个过程包含下面一个或多个机制:

(1)传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。

(2)传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。

(3)分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

hello程序中的函数有main、puts、exit、printf、atoi、sleep、getchar函数。

在程序入口处调用了main函数,其余函数都是通过call指令来调用的。注意,在源代码中并未调用puts函数,它是由于第一个printf函数只用来输出一串字符串,而被系统优化成了puts函数。

3.4 本章小结

本章介绍了编译的概念和作用,编译将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做了准备。此外,本章结合hello.s文件,对汇编代码中的一些C数据与操作进行了解析,介绍了编译器如何处理各个数据类型以及各类操作。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编是指汇编器(as)将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在.o目标文件的过程。作用:将编译器产生的汇编语言进一步翻译为计算机可以理解的二进制机器语言,生成.o文件。

4.2 在Ubuntu下汇编的命令

命令:as hello.s -o hello.o或 gcc -c hello.s -o hello.o

图4.1 汇编命令

4.3 可重定位目标elf格式

用readelf -a -W hello.o > hello.elf命令得到hello.elf文件:

图4.2 用readelf命令得到hello.o的ELF格式文件

接下来为其各节的基本信息:

4.3.1 ELF头

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小(64字节)、目标文件类型、机器类型(X86-64)、节头部表的文件偏移(1240字节),以及节头部表中条目的大小和数量等信息。

图4.3 ELF头

4.3.2节头部表

节头部表包含了文件中出现的各个节的名称、类型、属性、地址、偏移量、大小等信息,目标文件中每个节的都有一个固定大小的条目(entry)。

图4.4 节头部表

该文件的节头部表中包含的信息具体见图4.4,依次为:

(0)无效段,类型为NULL。

(1).text节:内容为已编译程序的机器代码。

(2).rela.text节:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

(3).data节:包含已初始化的全局和静态C变量。

(4).bss节:包含未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。

(5).rodata节:包含只读数据,如printf语句中的格式串和开关语句的跳转表等。

(6).comment节:存储的是编译器的版本信息。

(7).note.GNU_stack节:标记可执行堆栈。

(8).note.gnu.property节。

(9).eh_frame节:处理异常。

(10).rela.eh_frame节:保存.eh_frame节的重定位信息。

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

(12).strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。

(13).shstrtab节:该节保存着节名称。

4.3.3重定位节

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。

图4.5 重定位节

所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.re1.text中。已初始化数据的重定位条目放在.rel.data中。

图4.5是重定位节的具体内容。需要重定位的是各个调用的函数和只读数据内容。其中,offset是需要被修改的引用的节偏移;symbol标识被修改引用应该指向的符号;type告知链接器如何修改新的引用;addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。ELF定义了32种不同的重定位类型,两种最基本的重定位类型包括R_X86_64_PC32(重定位使用32位PC相对地址的引用)和R_X86_64_32(重定位使用32位绝对地址的引用)。

4.3.4 符号表

符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。

图4.6 符号表

name是字符串表中的字节偏移,指向符号的以nu11结尾的字符串名字。value 是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size是目标的大小(以字节为单位)。 type通常要么是数据,要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。binding字段表示符号是本地的还是全局的。

4.4 Hello.o的结果解析

用objdump -d -r hello.o > hello.txt 反汇编,得到hello.txt文件。

hello.o的反汇编得到的汇编代码基本与hello.s一致,每个操作代码都可以一一对应。此外,两者之间有一些细小的差别:

(1)立即数 在.s文件中,立即数直接以十进制数表示,而在反汇编得到的文件中,立即数以十六进制表示。

(2)指令表示 反汇编的指令mov、add、sub等后面没有对字大小的表示,如w,l,q等;但.s文件中有这些表示。而call指令在反汇编中表示为callq。

(3)分支转移 在.s文件中,跳转指令的目标地址直接记为段名称,如.L2,.L3等;而反汇编代码跳转指令的操作数使用的是确定的地址。

(4)函数调用 在hello.s文件中,call之后直接跟着函数名称,而在反汇编文件中,call后跟着的是直接的地址偏移。

 

                                                        图4.7 hello.o的反汇编结果

                                                        图4.8 hello.s代码段

4.5 本章小结

本章介绍了汇编的概念与作用,并依据实际操作将hello.s文件翻译为hello.o文件,并生成hello.o的ELF格式文件hello.elf,对ELF格式文件的具体结构进行了详细的分析。此外,将hello.o文件反汇编并与hello.s文件进行了对照分析,并就二者的差异进行说明。

(第4章1分)

5链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

作用:链接可以通过符号解析、重定位等工作将多个.o文件和静态库文件、动态链接库等合并得到一个执行文件,这使得分离编译成为可能,从而可以避免因某一个模块的小改动而需要重新编译所有文件的麻烦。

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

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

用readelf -a -W hello > helloprog.elf命令得到helloprog.elf文件:

                                图5.2 用readelf命令得到hello.o的ELF格式文件

hello的ELF格式文件的各节的基本信息:

5.3.1 ELF头

图5.3.1 ELF头

5.3.2 节头部表

图5.3.2 节头部表

其比hello.o文件多了一些段,各段的基本信息均已在上图中列出,不再赘述。    

5.3.3 程序头

图5.3.3 程序头

5.3.4 动态节

图5.3.4 动态节

5.3.5 重定位表

 

图5.3.5 重定位表

5.3.6 符号表

 

 图5.3.6 符号表

5.4 hello的虚拟地址空间

使用edb加载hello,通过Data Dump可以查看到本进程的虚拟地址空间各段信息。结合5.3的内容,可以定位到各节的信息。

程序头表(图5.5)中load表明程序段开始的地址为0x400000,见下图。

图5.4.1 edb中Data Dump部分示图

下图是.text节和.got节的信息:

 

图5.4.2 .text节(地址0x4010f0)

 

图5.4.3 .got节(地址0x403ff0)

5.5 链接的重定位过程分析

用命令objdump -d -r hello > helloprog.txt得到helloprog.txt文件。

相比于hello.o的反汇编文件,helloprog.txt中除了main函数,还有其调用的库函数(图5.5.1)和一些新增节,如.init节、.plt节(图5.5.2)。

图5.5.1 hello反汇编中的库函数(部分)

链接的过程:链接主要分两个过程:符号解析和重定位。

符号解析:目标文件定义和引用符号,每个符号对应于一个函数、-个全局变量或一个静态变量(即 C语言中任何以static属性声明的变量)。符号解析可以将每个符号引用正好和一个符号定义关联起来。

重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义于一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。重定位由下面两步组成:

重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成一个节,这个节成为输出的可执行目标文件的.data节。然后,链接器运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号.当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。

重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中的重定位条目。代码的重定位条目被放在.rel.text中,已初始化的 数据的重定位条目放在.rel.data中。

图5.5.2 hello反汇编中的新增节(部分)

重定位的算法见下图:

图5.5.3 重定位算法

对于hello在执行重定位,以hello.o反汇编文件第16行内容来举例说明。该行有一个重定位条目:类型为R_X86_64_PC32,offset为0x1c,addend为-4,symbol为.rodata(见图5.3.4);ADDR(main)和ADDR(.rodata)均可在hello的重定位表或者hello执行文件的反汇编文件中得到,再利用图5.3.3中6~8行算法即可得到更新的内容(一个相对地址)。

 

图5.5.4 重定位条目

5.6 hello的执行流程

子程序名

程序地址

hello!_start

0x4010f0

hello!main

0x401125

hello!puts@plt

0x401030

hello!printf@plt

0x401040

hello!atoi@plt

0x401060

hello!sleep@plt

0x401080

hello!getchar@plt

0x401050

hello!exit@plt

0x401070

表5.6 hello执行流程

5.7 Hello的动态链接分析

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,于是需要一条重定位记录由动态链接加载时来处理。动态链接器使用PLT和GOT来延迟绑定。

由图5.3.2的节头部表可知,.got.plt的起始位置是0x404000,在调用dl_init前后,其值发生了改变(图5.7),即实现了在运行时的链接。

图5.7.1 .got.plt内容(改变前)

 

图5.7.2 .got.plt内容(改变后)

5.8 本章小结

本章主要介绍了链接的概念与作用,对链接得到的可执行文件得的ELF格式文件进行详细分析,并于hello.o的文件进行比较,从而更加具体地表现了链接的执行操作和功能,并具体地介绍、表现了其重定位的原理和过程。

(第5章1分)

6hello进程管理

6.1 进程的概念与作用

概念:进程是操作系统对一个正在运行的程序的一种抽象。其经典定义就是一个执行中程序的实例。

作用:进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。它提供给应用程序两个关键抽象:1)一个独立的逻辑控制流,提供一个程序独占处理器的假象;2)一个私有的地址空间,提供一个程序独占地使用内存系统的假象。

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

Shell是一个交互型应用级程序,代表用户运行其他程序。如Windows下的命令行解释器,cmd、powershell,图形界面的资源管理器。Linux下的Terminal/tcsh、bash等等,也包括图形化的GNOME桌面环境。Shell是信号处理的代表,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。

处理流程:

1、读取用户由键盘输入的命令行。

2、分析命令,以命令名作为文件名,并将其它参数改造为系统调用execve( )内部处理所要求的形式。

3、终端进程调用fork( )建立一个子进程。

4、终端进程本身调用waitpid()来等待子进程完成(如果是后台命令,则不等待)。当子进程运行时调用execve(),子进程根据文件名到目录中查找有关文件,调入内存,执行这个程序。

5、如果命令末尾有&,表示后台执行,此时shell不会等待它完成;否则shell会等待其完成。当子进程完成工作后,向父进程报告,此时终端进程醒来,作必要的判别工作后,终端发出命令提示符,重复上述处理过程。

6.3 Hello的fork进程创建过程

在shell上输入./hello命令时,先判断该命令是否为内置命令,如果是内置命令则立即对其进行解释;不是否则将其看成一个可执行目标文件,再调用fork创建一个新进程并在其中执行。新创建的子进程鸡湖但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。二者之间的最大的区别是它们有不同的PID。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件,且带参数列表rgv和环境变量列表envp。只有当出现错误时,execve才会返回到调用程序。 

在子进程调用该函数时,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序;接着映射私有区域,为新程序的代码、数据等创建新的区域结构;然后映射共享区域,比如动态链接到程序中的标准C库libc.so等;再是设置程序计数器,使之指向代码区域的入口。

6.5 Hello的进程执行

在fork了一个子进程之后,该进程有了一个独立的逻辑控制流。在运行过程中&#x

以上是关于csapp 程序人生 Hello’s P2P的主要内容,如果未能解决你的问题,请参考以下文章

哈工大CSAPP大作业:程序人生-Hello’s P2P

hit csapp大作业 程序人生-Hello’s P2P

CSAPP程序人生-Hello’s P2P

程序人生-Hello’s P2P(CSAPP大作业)

程序人生-Hello’s P2P

程序人生-Hello’s P2P