HIT计统大作业——程序人生

Posted sltbsxxs

tags:

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

 

 

 

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业    计算机科学与技术    

学     号       2021113008       

班   级        2103102         

学       生         石隆泰   

指 导 教 师          刘宏伟     

计算机科学与技术学院

2022年5月

摘  要

多少人的代码生涯,都是从那一句“Hello World!”开始的?多少菜鸟手敲的第一个程序就是hello?可渐渐的,随着程序猿们知识越加丰厚,曾经的伙伴hello早已被抛之脑后。但是CS知道它的生,知道它的死,知道它来过。本文通过一个入门级程序hello.c,来剖析它坎坷的一生:从源代码到经过预处理、编译、汇编、链接最终生成可执行目标文件hello,再通过在shell 中键入启动命令后,shell 为其fork,产生子进程,内核为新进程创建数据结构, hello从可执行程序变成为进程。

关键词:Hello;预处理;编译;汇编;链接;进程                            

目  录

第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本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P(From Program to Process):用户通过编辑器编写人类可读的高级语言代码,得到一个hello.c程序(program)。为了在系统上运行hello程序,首先通过预处理器(cpp)得到修改了的源程序——ASCII码的中间文件hello.i,然后利用编译器(ccl)得到汇编程序——ASCII汇编语言文件hello.s,随后,再利用汇编器(as)生成可重定位目标程序hello.o(二进制)。接下来,经过链接器(ld),将调用的标准C库中的函数(如printf等)对应的预编译好了的目标文件以某种方式合并到hello.o文件中,得到可执行目标程序hello。此时即可把可执行文件加载到内存中,由系统执行,即成为一个进程(process)。

                     图1:编译系统  

020(From Zero-0 to Zero-0):当输入“./hello”后,shell调用fork()函数创建子进程,子进程通过execve加载并执行该程序时,映射虚拟内存,程序开始时载入物理内存,进入CPU处理。CPU为执行文件hello分配时间片,进行取指、译码、执行等流水线操作。内存管理器和CPU在执行过程中通过L1、L2、L3三级缓存和TLB多级页表在物理内存中取的数据,通过I\\O系统根据代码指令进行输出。程序运行结束后,shell父进程会回收这个僵死进程,内核会从把它从系统内清除,这是hello由0转换为0,完成020的过程。

1.2 环境与工具

硬件环境

X64 CPU;3GHz;16G RAM;256GHD Disk 以上;

软件环境

Windows10 64位;VirtualBox/Vmware 16;kali linux;

开发工具

Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 20.04 LTS 64位;edb;gdb;gcc;ld;objdump;readelf;HexEdit

1.3 中间结果

hello.c  源程序:能够被人读懂的c语言程序,以字节序列的方式储存

hello.i  预处理后文件:读取了相关头文件的内容

hello.s  编译后的汇编文件:包含了用汇编语言表示的main函数定义

hello.o  汇编后的可重定位目标执行文件:可被链接以待后续加载执行

hello  链接后的可执行文件:可以被加载到内存中由系统执行

elf.txt  hello.o的ELF

hello1.elf  hello的ELF

hello_asm.txt hello的反汇编文件

1.4 本章小结

本章简单介绍了Hello的P2P,020的过程,随后介绍了本文用到的环境与工具,以及撰写本文之后会生成的中间结果文件,为下文做铺垫。


第2章 预处理

2.1 预处理的概念与作用

概念:预处理是指预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行中的 #include <stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果是得到了另一个C程序,通常以.i作为文件扩展名。

作用:主要作用有宏定义、文件包含、条件编译。

宏定义:将所有的#define删除,并且展开所有的宏定义,将宏名替换为文本。.

文件包含:预处理程序中的#include,将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件。

条件编译:处理所有条件预编译指令,根据#if以及#endif和#ifdef以及#ifndef来判断执行编译的条件。

2.2在Ubuntu下预处理的命令

Ubuntu下预处理的命令是:cpp hello.c > hello.i 或者 gcc -E hello.c  -o hello.i

图2:Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

打开hello.i后,发现原本普普通通仅仅四行的代码被拓展到了3000多行。比较hello.i的尾部文件和hello.c,会发现“#”宏定义消失了。事实上,预处理是遵从对于资源的等价交换,占据hello.i大部分空间的代码实际上是对头文件的展开,对引用目录的标注,诸如”/usr/include/stdio.h”。

        

  

接着比较尾部文件和.c文件会发现,除上述变化以外,函数主体部分并没有改变。

 

图3:hello.i与hello.c的内容对比

2.4 本章小结

本章主要围绕预处理进行介绍,在明白预处理概念与作用之后学习Ubuntu下预处理的命令。并以hello.c为例操作生成hello.i。打开解析hello.i文件,对比原.c文件发现不同,加深了对预处理的理解。


第3章 编译

3.1 编译的概念与作用

概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序.该程序包含函数main的定义,这个过程称为编译。

作用:将高级语言源程序翻译成等价的目标程序,并且进行语法检查、调试措施、修改手段、覆盖处理、目标程序优化等步骤。此阶段编译器会完成对代码的语法和语义的分析,生成汇编代码,并将这个代码保存在hello.s文件中。

3.2 在Ubuntu下编译的命令

Ubuntu下编译的命令是:cc1 hello.i -o hello.s 或者gcc -S hello.i -o hello.s

 

图4:Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1文件声明

 

图5:hello.s头部文件

所有以‘.’开头的行都是指导汇编器和链接器工作的伪指令。

.file:声明源文件

.text:代码节

.rodata:只读代码段

.align 8:数据或者指令的地址对齐方式

.string:声明一个字符串(.LC0,.LC1)

.global:声明全局变量(main)

.type:声明一个符号是数据类型还是函数类型

3.3.2数据

(1)整型数据                

                        

 

图6:main()的参数传递

如图6可见main()函数的两个参数。

(2)字符串与立即数

图7:hello.s一部分头部文件

如图7,源程序中的两个字符串信息都被存储在.rodata段,而立即数会直接作为代码的一部分保存在.text section中。

(3)局部变量

                  

 

图8:局部变量保存

如图8,int i当前被保存在-4(%rbp)中,由图5可知%rbp是指向栈的寄存器,所以i被保存在栈上,其在初始化时是被保存在寄存器中的。

3.3.3赋值操作

                  

 

图9:赋值操作

赋值操作主要有mov指令实现,有以下几种:

movb:一个字节

movw:两个字节

movl:四个字节

movq:八个字节、

如图9,i=0这条赋值操作就是利用的movl指令来实现。

3.3.4算术操作

                  

 

图10:算术运算

汇编中有add,sub,imul,xor等算术操作对应着c语言程序中的+、-、*、异或等操作,在hello源程序中有对循环变量进行+1的操作,对应到汇编中是通过addl指令完成的,如图10的第二行和最后一行。

3.3.5控制转移

              

 

图11:控制转移

其中cmpl比较了立即数4与-20(%rbp)处值的大小,为一个关系操作其会通过一次计算来设置条件码,而je通过前一次操作设置的条件码实现当-20(%rbp)中的值也就是第一个参数argc不等于4时,跳转到.L2处,其为控制转移。

3.3.6关系操作

            

 

图12:关系操作

cmpl同样比较了立即数7与-4(%rbp)中值的大小并设置条件码,根据条件码判断,当-4(%rbp)中的值小于等于7时,程序会跳转到.L4处,即C程序中的i<8时执行for循环中的内容。

3.3.7指针数组

                      

 

图13:指针数组

如图13,main的第二个参数*argv[]是一个指针数组的首地址,它被保存在了寄存器%rsi中。

3.3.8函数操作

在hello.s中涉及的函数操作有:

main函数,printf,exit,sleep ,getchar函数;

main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串;

exit参数是1,sleep函数参数是atoi(argv[3]);

函数的返回值存储在%eax寄存器中。

                  

图14:if中printf与exit函数调用

 

                  

图15:主循环中printf函数调用

 

图16:atoi函数调用

hello.c中涉及的类型转换函数是:atoi(),将字符串类型转换为整数类

 

图17:sleep函数调用

 

图18:getchar函数调用

 

3.4 本章小结

本章主要介绍了编译的的概念以及编译的作用与功能,以及在Ubuntu下将hello.i文件编译生成hello.s文件的命令。

按照C语言的不同数据与操作类型,分析了源程序hello.c文件中的语句是怎样转化为hello.s文件中的语句的。其中数据类型包括数字常量、字符串常量和局部变量;操作类型包括赋值、类型转换、算术操作、关系操作、数组\\指针\\结构操作控制转移以及函数调用。


第4章 汇编

4.1 汇编的概念与作用

概念:汇编是指经过编译后,汇编器as将汇编语言翻译成机器语言,得到可重定位目标文件的过程。

作用:将汇编进一步翻译为计算机可以理解的二进制机器语言,这是机器可直接识别执行的代码文件。

4.2 在Ubuntu下汇编的命令

Ubuntu下汇编的命令 as hello.s -o hello.o 或者 gcc -c hello.s -o hello.o

 

图19:Ubuntu下的汇编命令

4.3 可重定位目标elf格式

 

图20:hello.o的elf头

ELF首先由一个16B的magic数,该数描述了生成该文件的系统的字的大小和字节顺序,当魔数出现异常时,操作系统会停止加载该程序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

 

图21:ELF节头部表

不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。节头记录每个节的名称、偏移量、大小、位置等信息。

.text节:已编译程序的机器代码以编译的机器代码。

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

.data节:已初始化的静态和全局C变量。

.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量,在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。

.rodata节:存放只读数据。

.comment节:包含版本控制信息。

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

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

.shstrtab节:该区域包含节的名称。

 

图22:重定位节

在.rela.text节中存放着代码的重定位条目。当链接器把这个目标文件和其他文件进行链接时,会结合这个节,修改.text节中相应位置的信息。

而重定位条目常见共2种:

R_X86_64_32:重定位绝对引用。重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

R_X86_64_PC32:重定位PC相对引用。重定位时使用一个32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。

可以看出,对于字符串的都是绝对引用。每个重定位条目包含如下信息:该节包括的内容是:偏移量,信息,类型,符号值,符名称和加数。

 

图23:符号表

“.symtab”是一个符号表,它给出了存放在程序中定义和引用的函数和全局变量的信息,但是它不会保存本地非静态数据,因为这些数据会在程序运行的时候被保存在栈或者寄存器中。在进行符号解析的时候需要用到符号表。

4.4 Hello.o的结果解析

 

图24:hello.o反汇编内容

对比hello.o的反汇编代码,与hello.s的内容进行比较,发现以下几点的不同:

  1. 数字进制不同

hello.s中操作数都是十进制的,而在反汇编代码中以十六进制表示,这对应着在机器中以二进制的形式存在。

  1. 对字符串常量的引用不同

hello.s中是用的全局变量所在的那一段的名称加上%rip的值,而hello.o中用的是0加%rip的值,因为当前为可重定位目标文件,之后还需经过重定位方可确定其具体位置,所以这里都用0来代替。

  1. 分支转移不同

hello.s中列出了每个段的段名,分支转移时,跳转指令后用对应的段的名称表示跳转位置;而在hello.o的反汇编代码中每个段都有明确的地址,跳转指令后用相应的地址表示跳转位置。

  1. 函数调用不同

在hello.s中调用函数时,在call指令之后直接引用函数名称,而在hello.o的反汇编代码中,在call指令后加上下一条指令的地址来表示。观察机器语言,发现其中操作数都为0,这是因为通过重定位信息,再链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用0代替。

4.5 本章小结

本章介绍了汇编的含义与作用,将hello.o转化成elf格式,主要分析了ELF头、节头部表、重定位节和符号表这几个节。之后通过objdump进行hello.o的反汇编与其hello.s进行对比,发现了其结构大致相同,但有些许差别,反汇编代码更加接近于底层,而汇编代码可读性更强。
第5章 链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合成一个文件的过程,这个文件可被加载到内存执行。链接可以执行于编译时、加载时、运行时。

作用:把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译成为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。

5.2 在Ubuntu下链接的命令

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

 

图25:Ubuntu下链接的命令

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

   

 

图26:hello的ELF头

   

 

   

图27:hello的节头部表

 

 

图28:hello的程序头部表

 

图29:hello的重定位节内容

 

5.4 hello的虚拟地址空间

  

       

 

图30:edb打开hello的Data Dump窗口

edb的Data Dump窗口。窗口显示虚拟地址由0x400000开始,从开始到结束这之间的每一个节对应5.3中的每一个节头表的声明。

 

 

图31:edb打开hello的Loaded Symbols窗口

证实从虚拟地址从0x400000开始和5.3节中的节头表是一一对应的。

5.5 链接的重定位过程分析

运用指令objdump -d -r hello > hello-asm.txt得到hello的反汇编文件。

 

图32:反汇编的命令

将hello.o与hello文件的反汇编代码进行对比分析,得到如下几方面的不同:

  1. 链接后函数数量增加。多出了puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。

                

 

  图33:反汇编文件部分内容(一)

  1. 函数调用指令call的参数发生变化。

 

图34:反汇编文件部分内容(二)

  1. 跳转指令参数发生变化。

 

图35:反汇编文件部分内容(三)

5.6 hello的执行流程

5.7 Hello的动态链接分析

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

在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。

通过节头表找到GOT起始位置0x404000

 

 

图36:got.plt起始位置

调用dl_init之的情况:

 

图37:调用dl_init之前的内容

调用dl_init之后的情况:

 

图38:调用dl_init之后的内容

5.8 本章小结

本章结合实验中的hello可执行程序依此介绍了链接的概念及作用以及命令,并对hello的elf格式进行了详细的分析对比。以及hello的虚拟地址空间知识,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,整理了过程中的子函数,在最后对hello进行了动态链接分析,对链接有了更深的理解。


6hello进程管理

6.1 进程的概念与作用

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

作用:进程提供独立的逻辑控制流,好像我们的程序独占地使用处理器;也提供一个私有的地址空间,好像我们的程序独占地使用内存系统;使CPU被科学有效地划分成多个部分以并行地运行多个进程。

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

作用:Shell为用户提供命令行界面,使用户可以在这个界面中输入shell命令,然后shell执行一系列的读/求值步骤,读步骤读取用户的输入的命令行,求值步骤则解析命令行,并运行程序。完成后重复上述步骤,直到用户退出shell。从而完成用户与计算机的交互来操作计算机。

处理流程:Shell打印一个命令行提示符,等待用户输入指令。在用户输入指令后,从终端读取该命令并进行解析,若该命令为shell的内置命令,则立即执行该命令;若不是内置命令,是一个可执行目标文件,则shell创建会通过fork创建一个子进程,并通过execve加载并运行该可执行目标文件,用waitpid命令等待执行结束后对其进行回收,从内核中将其删除;若将该文件转到后台运行,则shell返回到循环的顶部,等待下一个命令行。完成上述过程后,shell重复上述过程,直到用户退出shell。

6.3 Hello的fork进程创建过程

终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

 

39:创建子程序

以 ./hello 2021113008 shilongtai 1 为例,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,如图39

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境遍历列表envp。只有当出现错误时,execve才会返回到调用程序。所以,execve调用一次并从不返回。在execve加载了filename后,调用启动代码,启动代码设置栈,并将控制转移传递给新程序的主函数。

子进程通过execve函数系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后加载器调到_start处,最终调用应用程序的main函数。

6.5 Hello的进程执行

当开始运行hello时,内存为hello分配时间片,如一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流,逻辑流的执行是交错的,它们轮流使用处理器,会存在并发执行的现象。其中,一个进程执行它的控制流的一部分的每一时间段叫做时间片。然后在用户态下执行并保存上下文。

如果在此期间内发生了异常或系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,控制将交付给其他进程。

当hello 执行到 sleep时,hello 会休眠,再次上下文切换,控制交付给其他进程,一段时间后再次上下文切换,恢复hello在休眠前的上下文信息,控制权回到 hello 继续执行。

hello在循环后,程序调用 getchar() , hello 从用户态进入核心态,并再次上下文切换,控制交付给其他进程。最终,内核从其他进程回到 hello 进程,在return后进程结束。

6.6 hello的异常与信号处理

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

 

图40:异常的类别

 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

         

 

图41:运行时不停乱按结果图

可以看到,虽然乱按不会影响程序的运行,但是会在程序运行结束后对shell发送许多无效指令,不过要注意因为hello程序最后有一个getchar,因此第一个乱按的指令被getchar给读走了,不会成为发送给shell的无效指令。因为我们可以判断,我们乱按的内容被放入缓冲区,等待程序执行结束被shell当作命令读走。

  

 

图42:运行时Ctrl-Z结果图

可以看到,ctrl+z后程序被放入后台并暂停运行。

 

   

图43:运行时Ctrl-C结果图

可以看到,ctrl+c后程序直接结束运行,回到shell等待输入下一条指令。

 

       

图44:Ctrl-Z后输入ps结果图

可以看到,输入ps,可以查看当前所有进程的相关信息,可以发现此时hello程序仍然存在。

 

      

图45:Ctrl-Z后输入jobs结果图

可以看到,输入jobs可以查看前台作业号。

 

图46:Ctrl-Z后输入pstree结果图

可以看到,输入pstree命令,以树形结构显示了程序和进程间的关系。

 

      
            图47:Ctrl-Z后输入fg结果图

可以看到,输入fg,能使被挂起的hello程序变成前台程序继续运行。

 

    

   图48:Ctrl-Z后输入kill结果图

对比kill前后两次ps显示的进程信息,可以知道,输入kill -9 4488会将SIGKILL信号发送给进程4488,使得它被终止。

6.7本章小结

本章主要介绍了进程的概念与作用。同时介绍了壳Shell-bash作用与处理流程。明确了hello的fork进程创建过程与execve过程,通

以上是关于HIT计统大作业——程序人生的主要内容,如果未能解决你的问题,请参考以下文章

HIT-CSAPP程序人生大作业

程序人生——Hello‘s P2P(HIT CSAPP大作业)

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

键盘敲击(keyboard hit)

求一C语言程序设计作业,工资管理系统

C语言编程作业,急!!!!