题 目 程序人生-Hello’s P2P

专 业 计算机科学与技术

学   号 2021112563

班   级 2103103

学 生  赵梓豪   

指 导 教 师 刘宏伟   

摘 要

本文主要阐述hello程序在Linux系统的生命周期,借助edb、gcc等工具探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程。同时比较全方面的涉及了Hello程序在其生命周期中可能出现的特殊情况以及处理方法等。

关键词: linux, ubuntu,汇编语言,操作系统,链接,进程

目 录

第1章 概述……………………………………………………………………….. – 4 –

1.1 Hello简介…………………………………………………………………. – 4 –

1.2 环境与工具………………………………………………………………… – 4 –

1.3 中间结果……………………………………………………………………. – 4 –

1.4 本章小结……………………………………………………………………. – 5 –

第2章 预处理……………………………………………………………………. – 6 –

2.1 预处理的概念与作用………………………………………………….. – 6 –

2.2在Ubuntu下预处理的命令………………………………………… – 6 –

2.3 Hello的预处理结果解析……………………………………………. – 6 –

2.4 本章小结……………………………………………………………………. – 7 –

第3章 编译……………………………………………………………………….. – 8 –

3.1 编译的概念与作用……………………………………………………… – 8 –

3.2 在Ubuntu下编译的命令……………………………………………. – 8 –

3.3 Hello的编译结果解析……………………………………………….. – 8 –

3.4 本章小结………………………………………………………………….. – 12 –

第4章 汇编……………………………………………………………………… – 13 –

4.1 汇编的概念与作用……………………………………………………. – 13 –

4.2 在Ubuntu下汇编的命令………………………………………….. – 13 –

4.3 可重定位目标elf格式……………………………………………… – 13 –

4.4 Hello.o的结果解析………………………………………………….. – 15 –

4.5 本章小结………………………………………………………………….. – 17 –

第5章 链接……………………………………………………………………… – 18 –

5.1 链接的概念与作用……………………………………………………. – 18 –

5.2 在Ubuntu下链接的命令………………………………………….. – 18 –

5.3 可执行目标文件hello的格式………………………………….. – 18 –

5.4 hello的虚拟地址空间………………………………………………. – 21 –

5.5 链接的重定位过程分析…………………………………………….. – 22 –

5.6 hello的执行流程……………………………………………………… – 24 –

5.7 Hello的动态链接分析……………………………………………… – 24 –

5.8 本章小结………………………………………………………………….. – 25 –

第6章 hello进程管理…………………………………………………. – 26 –

6.1 进程的概念与作用……………………………………………………. – 26 –

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

6.3 Hello的fork进程创建过程…………………………………….. – 26 –

6.4 Hello的execve过程……………………………………………….. – 27 –

6.5 Hello的进程执行…………………………………………………….. – 27 –

6.6 hello的异常与信号处理…………………………………………… – 28 –

6.7本章小结…………………………………………………………………… – 31 –

结论………………………………………………………………………………….. – 33 –

附件………………………………………………………………………………….. – 34 –

参考文献…………………………………………………………………………… – 35 –

第1章 概述

1.1 Hello简介

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

P2P(From Program to Process)

首先编写高级语言程序得到hello.c文件,也就是最初的Program;然后运行cpp将hello.c文件进行预处理,得到hello.i文件;然后运行cc1编译器编译得到hello.s汇编文件;之后运行汇编器as得到hello.o可重定位目标文件;最后运行链接器ld,将hello.o与库函数链接,创建可执行文件hello。在shell进程中输入命令linux>./hello,shell会调用fork函数产生子进程来执行hello。于是hello完成了From Program to Process。

020(From Zero-0 to Zero-0)

子进程调用execve加载hello,重新为hello进行内存映射, 设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。进入程序入口后通过存储管理机制将指令和数据载入内存,CPU为hello分配时间片执行逻辑控制流。程序执行过程中,通过I/O系统进行输入输出。hello执行完成后操作系统回收hello进程,内核从系统中删除hello所有相关数据。hello程序从最开始的不存在于虚拟内存中到被回收后不再存在于虚拟内存中,就是From Zero-0 to Zero-0。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:CPU:AMD Ryzen 7 5800H;内存:16GB;磁盘:512GB SSD

软件环境:Window10 64bits; Ubuntu 20.04.5 LTS 64bits

开发工具:vim , gcc ,edb ,readelf,objdump

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c 源程序

hello.i 预处理后文件

hello.s 编译后的汇编文件

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

hello 链接后的可执行文件

elf1.txt hello.o 的ELF文件

elf2.txt hello 的ELF文件

1.4 本章小结

本章介绍了P2P,020,介绍了本次大作业的软硬件环境及中间结果。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。

作用:将源程序开头声明的头文件插入到hello.c中,对宏定义的内容进行对应处理,得到hello.i。

2.2在Ubuntu下预处理的命令

gcc -E -o hello.i hello.c

2.3 Hello的预处理结果解析

通过vim可以查看hello.c 和hello.i的内容,如图。

程序变成了三千多行,原来出现在最开头的三个头文件已经消失,被其他文本代替。

所有头文件的代码都被写入了hello.i当中,gcc还将所有的注释删除了。

2.4 本章小结

本章对预处理得到的文件进行初步分析。通过预处理,hello.c文件变成了脱离头文件,可以独立进行编译的文件,并删除掉了注释。

第3章 编译

3.1 编译的概念与作用

概念:将源语言经过词法分析、语法分析、语义分析以及一系列优化后生成汇编代码的过程。编译器将预处理得到的文本文件 hello.i 翻译成文本文件 hello.s。

作用:将源语言经过词法分析、语法分析、语义分析以及一系列优化后生成汇编代码,即将高级语言程序转化为机器可直接识别处理执行的的机器码的中间步骤。

词法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位。

语法分析:在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。

语义分析:即静态语法检查,对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。

代码优化:对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码

3.2 在Ubuntu下编译的命令

gcc -S -o hello.s hello.i

3.3 Hello的编译结果解析

3.3.1 hello.s中的内容

.file 声明源文件

.text 声明代码段

.section .rodata 声明只读数据

.globl 声明全局变量

.align 声明对齐方式

.string 声明一个string类型

.size 声明大小

.type 声明类型是数据还是函数

3.3.2数据

局部变量:int i; i被保存在%rbp-4的位置

数组char *argv[];

L4代码端其实就是for循环体内容

33-36就是将argv[2]赋值给%rdx(作为第3个参数)

37-40就是将argv[1]赋值给%rsi(作为第2个参数)

可以看出argv数组的首地址存放在栈中%rbp-32的位置,是一个字符指针数组

字符串:有两个字符串,都存放在只读数据段,地址作为参数

全局符号:函数main属于全局符号

3.3.3赋值操作

hello中的赋值操作主要有:i=0,而这个操作在.s文件中汇编代码主要使用mov指令来实现。mov指令根据操作数的字节大小分为:movb:一个字节赋值,movw:两个字节赋值,movl:四个字节赋值,movq:八个字节赋值。

3.3.4算数操作

hello.c中的算数操作主要有i++,i是int类型,在汇编代码中用addl实现此操作。

3.3.5 关系操作

hello.c中 “if(argc!=4)”,这是一个条件判断语句,在进行编译时,被编译为:cmpl$4, -20(%rbp)。比较后设置条件码,根据条件码判断是否需要跳转。

hello.c源程序中的for循环条件是for(i=0;i<8;i++),该条指令被编译为cmpl$7,-4(%rbp)。同样在判断后设置条件码,为下一步的jle利用条件码跳转做准备。

3.3.6 控制转移操作

je:比较argc和4,如果等于则跳转到.L2

jle:比较i和7,如果i小于等于7,则跳转到.L4

jmp:无条件跳转

3.3.7 函数操作

使用call进行跳转,事先约定%rax为返回值 %rdi为第一个参数 %rsi为第二参数

3.4 本章小结

本章内容围绕编译展开,介绍了编译的概念以及作用,说明了ubuntu下的编译命令,对hello.i程序进行编译获得了hello.s程序,并对hello.s程序进行了解析,分析了其数据、操作等,加深了对汇编文件的理解。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。

作用:汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令

gcc -c -o hello.o hello.s

4.3 可重定位目标elf格式

指令:readelf -a hello.o > elf1.txt

将hello.o的elf文件内容显示到elf1.txt里

1.ELF头:以 16B 的序列 Magic 开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,余下部分指明了类别,数据,版本,OS/ABI,ABI,ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息

2. 节头表:节头表包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度以及节的对齐方式和偏移量。

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

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

name:符号名称;value:对于可重定位目标模块,value是符号相对于目标节的起始位置偏移;size:目标的大小;type:类型,数据或函数;Bind:表明符号是本地的还是全局。

4.4 Hello.o的结果解析

指令:objdump -d -r hello.o

结果如图

与hello.s进行对照分析

反汇编代码与汇编代码并无太大修改,只是把汇编语言转化成了机器码。主要差别有:

1.立即数变为16进制

2.函数调用

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

3.跳转指令

反汇编代码跳转指令的操作数使用的不是段名称,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

4.5 本章小结

本章主要介绍了汇编文件hello.o,以及汇编的概念与作用,如何得到汇编文件的操作命令。同时对elf文件做了详细的分析,也比对了hello.o的反汇编文件与之前得到的hello.s文件,使得我们对这部分内容有了更加深入的理解。

第5章 链接

5.1 链接的概念与作用

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

作用:链接器使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨 大的源文件,更便于我们维护管理,可以独立的修改和编译我们需要修改的小的模块。

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.3 可执行目标文件hello的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

1.指令:readelf -a hello > elf2.txt

2.ELF头

3. 节头表:各节的基本信息均在节头表(描述目标文件的节)中进行了声明。节头表(包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息)

4.重定位节

5.符号表

5.4 hello的虚拟地址空间

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

1.在data dump中查看加载到虚拟地址情况:

2. 各节位置和名称:地址和名称与5.3相对应

3. 利用节头表可以去到各个节,比如.text节:

ELF文件中显示.text节的地址为0x4010f0,大小为0x145

在edb中也可以找到

5.5 链接的重定位过程分析

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

指令执行结果

与hello.o的不同:hello.o没有经过链接,所以main的地址从0开始,并且不存在调用的如printf这样函数的代码。hello将hello.o需要重定位的部分进行了重定位,并将其地址转变成了虚拟空间中绝对地址,并在后面标出执行函数的地址和函数名;将库函数添加到了文件中,并添加了节。

hello的重定位过程:

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

然后,重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。

5.6 hello的执行流程

1.开始执行::_start、_libc_start_main

2.执行main:_main、_printf、_exit、_sleep、_getchar

3. 退出:exit

程序名 程序地址

_start 0x4010f0

_libc_start_main 0x2f12271d

main 0x401125

_printf 0x401040

_exit 0x401070

_sleep 0x401080

_getchar 0x401050

5.7 Hello的动态链接分析

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

.got.plt起始表的位置为0x404000。

GOT表调用dl_init前0x404008后的16个字节均为0;

调用dl_init后的.got.plt:

.got.plt的条目已经发生变化。

5.8 本章小结

本章围绕链接展开,阐述了链接的概念以及作用,说明了ubuntu下的链接指令,并对hello的ELF格式各个部分进行了分析,还使用edb加载hello,查看本了进程的虚拟地址空间各段信息,并通过反汇编hello文件,将其与hello.o反汇编文件进行了对比,详细了解了重定位过程;此外,还遍历了整个hello的执行过程,在最后对hello进行了动态链接分析。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

作用:它提供一个假象,好像我们的程序独占地使用内存系统,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

作用:

Shell是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作。而NU组织发现sh是比较好用的又进一步开发Borne Again Shell,简称bash,它是Linux系统中默认的shell程序。

处理流程:

1.从终端读入输入的命令。

2.切分命令行字符串,获取命令行参数

3.检查第一个命令行参数是否是一个内置的shell命令,是则立即执行,如果不是,则用fork创建子程序

4.子进程中,进行步骤2获得参数,调用exceve()执行制定程序

5.命令行末尾没有&,代表前台作业,shell使用waitpid等待作业终止后返回

6.命令行末尾有&,代表后台作业,shell返回

6.3 Hello的fork进程创建过程

在终端中输入./hello 学号 姓名 1命令后,shell会处理该命令,判断出不是内置命令,则会调用fork函数创建一个新的子进程,子进程几乎但不完全与父进程相同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是虚拟地址独立、PID也不相同的一份副本。

6.4 Hello的execve过程

调用fork函数之后,子进程将会调用execve函数,来运行hello程序,如果成功调用则不再返回,若未成功调用则返回-1。

完整的加载运行hello程序需要以下几个步骤

1.首先加载器会删除当前子进程虚拟地址端。,然后创建一组新的代码、数据、堆端,并初始化为0。

2.接着映射私有区域和共享区域,将新的代码和数据段初始化为可执行文件中的内容

3.最后设置程序计数器,使其指向代码区的入口,下一次调度这个进程时,将直接从入口点开始执行

6.5 Hello的进程执行

逻辑控制流和时间片:

进程的运行本质上是CPU不断从程序计数器 PC 指示的地址处取出指令并执行,值的序列叫做逻辑控制流。操作系统会对进程的运行进行调度,执行进程A->上下文切换->执行进程B->上下文切换->执行进程A->… 如此循环往复。 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。

用户模式和内核模式

Shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。

上下文切换:

如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。

调度:

在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。

用户态与核心态转换:

为了能让处理器安全运行,需要限制应用程序可执行指令所能访问的地址范围。因此划分了用户态与核心态。

核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理

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

hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。

中断:来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。

陷阱:有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。

故障:由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。

终止:不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。

1.正常停止:hello进程被回收,ps中无hello进程

2.ctrl+Z操作:向进程发送了一个sigtstp信号,让进程暂时挂起,输入ps命令符可以发现hello进程还没有被关闭。

3. fg命令:使后台挂起的进程继续运行。

4. ctrl+C操作:向进程发送一个sigint信号,让进程直接结束,输入ps命令可以发现当前hello进程已经被终止了。

5. jobs命令:可以查看当前的关键命令

6. pstree命令:用进程树的方法把各个进程用树状图的方式连接起来

7. kill命令:向固定进程发送某些信号,kill -s 30 3606,就表示向PID为3606的进程hello,发送了一个电源故障的信号,然后用fg命令让他继续运行就会出现故障内容提醒。

6.7本章小结

本章主要介绍了进程的概念及其作用,对shell的功能和处理流程也进行了介绍,然后详细分析了hello程序从fork进程的创建,到execve函数执行,最后具体执行过程以及出现异常的处理。对于整个进程管理有了更加深入的理解。

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

hello的一生经历了被编写成代码、预处理、编译、汇编、链接、运行、创建子进程、加载、执行指令、访问内存、动态内存分配、发送信号、终止。

hello利用它充实的一生向我们展示了一个程序的生命历程,带我们加深了对计算机系统的理解。

1.预处理:从.c生成.i文件,将.c中调用的外部库展开合并到.i中

2.编译:由.i生成.s汇编文件

3.汇编:将.s文件翻译为机器语言指令,并打包成可重定位目标程序hello.o

4.链接:将.o可重定位目标文件和动态链接库链接成可执行目标程序hello

5.运行:在shell中输入命令

6.创建子进程:shell用fork为程序创建子进程

7.加载:shell调用execve函数,将hello程序加载到该子进程,映射虚拟内存

8.执行指令:CPU为进程分配时间片,加载器将计数器预置在程序入口点,则hello可以顺序执行自己的逻辑控制流

9.访问内存:MMU将虚拟内存地址映射成物理内存地址,CPU通过其来访问

10.动态内存分配:根据需要申请动态内存

11.信号:shell的信号处理函数可以接受程序的异常和用户的请求

12.终止:执行完成后父进程回收子进程,内核删除为该进程创建的数据结构

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c 源程序

hello.i 预处理后文件

hello.s 编译后的汇编文件

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

hello 链接后的可执行文件

elf1.txt hello.o 的ELF文件

elf2.txt hello 的ELF文件

参考文献

[1] Randal E. Bryant, David R. O’Hallaon. 深入理解计算机系统. 第三版. 北京市:机械工业出版社[M]. 2018: 1-737