计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 xxxx
班 级 xxxx
学 生 xxxx
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文运用gcc,edb等工具,并结合CSAPP的相关内容,对Hello的整个生命周期进行了一个系统的分析和理解。从最初的编写到预处理、编译、汇编,到最后链接生成可执行目标文件,这一整个过程在本文中都有着较为全面的撰写,带大家走近Hello的一生。
关键词:预处理;编译;汇编;链接;进程;
目 录
第1章 概述
1.1 Hello简介
1.1.1Hello的P2P(From Program to Process)过程:
在编译器中编写C程序源代码,得到最初的hello.c文件,即最初的Program。从源文件到目标文件的转换是由编译器驱动程序来完成的。在该过程中用户在需要调用预处理器、编译器、汇编器和链接器来完成。驱动程序首先运行预处理器(cpp),将C程序的源程序hello.c翻译成一个ASCII码的.i文件;然后运行编译器(cc1)将.i文件翻译成一个ASCII汇编语言文件,即.s文件;之后运行汇编器(as)将.s文件翻译成可重定位目标文件,即.o文件;最后运行链接器(ld)创建一个可执行目标文件hello。在shell中输入执行hello的命令,shell解析命令行,通过调用fork( )函数创建一个子进程来执行hello,这时Hello已从Program转换为了Process。
1.1.2 Hello的020(From Zero-0 to Zero-0)过程:
子进程调用execve( )映射虚拟内存,设置当前进程上下文中的程序计数器PC,使之指向代码区域的入口点。进入程序入口后通过存储管理机制将指令和数据载入内存,CPU以流水线形式读取并执行指令,执行逻辑控制流。操作系统负责进程调度,为进程分时间片。执行过程中通过L1、L2、L3高速缓存、TLB、多级页表等进行存储管理,通过I/O系统进行输入输出。当程序运行结束后,父进程回收hello进程和它创建的子进程,内核删除相关数据结构。Hello从开始的未被内存映射到运行再到回收后不再存在,就是020的过程。
1.2 环境与工具
1.2.1硬件环境
X64 CPU;
1.2.2软件环境
Windows10 64位
1.2.3开发工具
Ubuntu 16.04 LTS 64位,edb;
1.3 中间结果
hello.c 源程序
hello.o 汇编后的可重定位目标执行文件
hello1.txt hello.o的反汇编代码
hello.i 预处理后文件
a.out 链接后的可执行文件
hello.s 编译后的汇编文件
hello.elf hello.o的ELF格式
hello1.elf hello的ELF格式
1.4 本章小结
本章简要描述了Hello的P2P、020的整个过程并介绍了实验的基本信息:环境、工具以及实验的中间结果。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是指计算机在编译之前对程序进行的处理。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,为接下来的编译做准备工作,但不做语法检查。预处理中会展开程序引用的所有库,处理所有的条件编译,并执行所有的宏定义,最后生成一个以.i为扩展名的C程序文件。
2.1.2 预处理的作用:
(1)宏替换:宏定义(#define语句)在预处理过程中会进行宏替换。宏定义分为不带参数的宏定义和带参数的宏定义两种形式。在不带参数的宏定义中,将#define定义的字符和字符串用实际值替换;在带参数的宏定义中,还需将参数进行代换;
(2)文件包含:预处理会对#include语句进行处理。将c程序中所有#include声明的头文件复制到新的程序中。将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件。
(3)条件编译:针对#if,#endif,#ifdef以及#ifndef等语句进行的处理。条件编译能够根据#if的不同条件决定需要进行编译的代码,#endif是结束这些语句的标志。使用条件编译可以使目标程序变小,在满足条件之后才会进行编译。
2.2在Ubuntu下预处理的命令
Linux下对Hello进行预处理:
具体指令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
hello.i文件部分截图如下:
结果分析:
预处理后hello.c文件生成了hello.i文件。打开hello.i文件发现它仍为C语言程序文本,但相比于hello.c文件内容却大大增加。由于预处理只是对源代码中的以字符“#”开头的语句进行处理,因此在预处理阶段程序中定义的其他操作并不会在预处理阶段进行处理。
2.4 本章小结
本章主要介绍了将hello.c文件生成可执行目标文件的预处理阶段。首先对预处理的概念和作用做了一个介绍,又结合具体的hello程序和生成的hello.o文件,对预处理进行过程分析和结果解析。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
编译就是将源C程序经过词法分析、语法分析、语义分析以及经过一系列优化后生成汇编代码的过程。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
3.1.2编译的作用
编译器生成汇编程序的过程划分为六个阶段:
- 扫描(词法分析):该阶段是对字符序列进行处理。将源代码程序输入扫描器,从左至右逐个字符地对源代码进行扫描,将源代码的字符序列分割成一系列记号;
- 语法分析:语法分析的任务是在词法分析的基础上将单词序列分解成各类语法短语,如“程序”、“语句”、“表达式”等。这种语法短语也称为语法单位,可以表示成语法树;
- 语义分析:语义包括动态语义和静态语义。语义分析的任务是审查源程序有无语义错误(但不判断程序对错),为代码生成阶段收集类型信息;
- 中间代码的生成:在经过上述步骤后,有些编译器会将源程序翻译成中间代码。中间代码是源程序的一种内部表示,或称中间语言。中间语言的复杂性介于源程序语言和机器语言之间。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,容易生成;且翻译成目标代码(即中间语言程序)较为容易;
- 代码优化:代码优化即是对生成的中间代码进行变换或改造,使得能够生成一个高效可执行代码,即目标代码运行时间较短,以及占用的存储空间较小;
- 目标代码生成:目标代码生成是编译的最后一个阶段。 这一阶段的任务是把中间代码变换成特定机器上的绝对指令代码或可重定位的指令代码或汇编指令代码。
汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
具体指令:gcc -S hello.c -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
- 字符串
hello.c中有两个字符串,且都存放在只读数据段,如图:
这两个字符串作为printf的参数,如下:
- 局部变量
在main函数中声明了一个局部变量,编译器进行编译时将局部变量i放在栈中-4(%rbp)的位置,如下图所示。
- 参数argc
main函数的第一个参数argv由用户输入,编译器进行编译时将其放入到了栈中。
- 数组参数char*argv[]
main函数的第二个参数是一个以指向字符型的指针为元素的数组,同样是由用户输入。数组的起始地址存放在在-32(%rbp)的位置,在内存中被两次调用穿给printf函数。
- 立即数
立即数在汇编代码中以$开头。
3.3.2赋值操作
赋值操作汇编代码主要使用MOV指令来实现。在x86-64下,数据传送指令有四个变种:movb(传送字节)、movw(传送字)、movl(传送双字)、movw(传送四字)。
hello.c中的赋值操作为i=0,如图所示:
3.3.3类型转换
由于在实际内存当中分配空间是不区分类型的,所以实现数据类型的转换本质是通过改变数据所占字节的大小来改变数据类型。一般通过MOVS和MOVZ类指令来实现,与MOV指令相同根据不同字节大小的转换有不同的后缀。hello.c中的atoi(argv[3])将字符串类型转换为int型。
3.3.4算术操作
算术操作在汇编代码中大多都分成了指令类,这些操作被分为四组:加载有效地址(leaq)、一元操作(INC/DEC/NEG)、二元操作(ADD/SUB/IMUL)和移位(SAL/SHL/SAR/SHR),且都有各种带不同大小操作数的变种。
hello.c中的算术操作有i++,由于i定义为int型,所以在汇编代码中用addl指令,如图所示:
3.3.5逻辑/位操作
与算术操作相同在汇编代码中大多都分成了指令类,且都有各种带不同大小操作数的变种。其中一元操作有取补指令(NOT),二元操作有异或操作(XOR)、或操作(OR)和与操作(AND)。
3.3.6关系操作
条件判断语句在汇编代码中一般使用CMP和TEST指令两类指令来设置条件码,然后根据条件码判断是否需要跳转。
hello.c中有两个条件判断语句:
第一个是if(argc!=4),编译器将其编译为cmpl$4, -20(%rbp),并设置条件码,根据条件码判断是否需要跳转。
第二个是for循环条件是for(i=0;i<9;i++),编译器将其编译为cmpl$8,-4(%rbp),并设置条件码,根据条件码判断是否需要跳转。
3.3.7数组/指针/结构操作
对于数组及结构体的实现,都是将其放入到一段连续的区域当中,而指向它们的指针就是数组或结构体的第一个字节的地址。hello.c中唯一的数组是main函数的第二参数,且指向字符类型的指针为数组元素。
3.3.8控制转移
汇编语言中设置了条件码,然后根据条件码来进行控制程序的跳转。其中包括进位标志CF、零标志ZF、符号标志SF、溢出标志OF。
在hello.c中有两个控制转移指令:
第一个是判断argc是否等于4。如果等于4,则不执行if语句;反之则执行后续的语句,对应的汇编代码如图所示:
第二个在for循环中,循环判断条件为i是否小于8。先对i进行赋初值,然后无条件跳转至判断条件的.L3中,然后判断i是否符合循环的条件,若符合则直接跳转到.L4中。这一部分的汇编代码如图所示:
3.3.9函数操作
hello.c中涉及的函数操作主要有以下几个:main,printf,exit,sleep,和getchar函数。main函数的参数是argc和*argv,printf函数的参数是字符串,exit函数的参数是1,sleep函数的参数是atoi(argv[3])。所有函数的返回值都会存储在寄存器%eax中。函数的调用与传参的过程是给函数传递参数需要先设定一个寄存器,将参数传给这个设定的寄存器后,再通过call来跳转到调用的函数开头的地址。
3.4 本章小结
本章主要介绍了编译的概念及其作用,以及在linux下编译的指令运行。最后根据hello.i文件生成的编译文件hello.s文件中的汇编代码详细解析了数据类型,各类操作在编译器中是如何实现的。经过这一章,hello.c已经被转换成了更底层的汇编程序。
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念:
汇编指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.1.2汇编的作用:
汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o(二进制文件)中。
4.2 在Ubuntu下汇编的命令
Linux下对Hello进行编译:
具体指令:gcc -c -o hello.o hello.s
4.3 可重定位目标elf格式
4.3.1 ELF头 :
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
4.3.2 节头:
记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
4.3.3重定位节:
重定位节.rel.text,一个.text节中位置的列表,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面调用本地函数的指令则不需要修改。Hello程序中需要被重定位的有printf和.rodata中的.L0和.L1。.rela.eh_frame节是.eh_frame节的重定位信息。
4.3.4符号表:
符号表.symtab,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
(1)指令格式
机器语言:
汇编语言:
正在上传…重新上传取消正在上传…重新上传取消
机器语言的指令不会因为数据不同的字节大小而有区别,汇编指令则会。
- 立即数的格式
机器语言:
汇编语言:
在机器语言中,立即数全部是以16进制表示的,因为16进制与2进制之间的转换比十进制更加方便,所以都转换成了16进制。
- 分支转移
机器语言:
汇编语言:
反汇编代码跳转指令的操作数使用的不是段名称,而机器语言直接是确定的地址。因为段名称只是在汇编语言中便于编写的助记符。
- 对函数的调用与重定位条目对应
机器语言:
汇编语言:
在机器语言中call后面不再是函数的具体名称,而是一条重定位条目指引的信息。
4.5 本章小结
本章主要介绍了汇编的概念与作用,并通过操作指令获得了汇编文件。同时对elf文件做了详细的分析,也将hello.o的反汇编文件与之前得到的hello.s文件进行了对比分析,加深了对汇编的了解。
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
5.1.2链接的作用:
链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接,并使得分离编译成为可能,我们可以独立的修改和编译需要修改的小的模块。
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
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1.elf
- ELF头:
Hello的文件头显示文件类型为可执行文件,有25个节,hello.o的文件头显示文件类型为可重定位文件,有13个节。
- 节头:对 hello中所有的节信息进行了声明,包括大小和偏移量。
- 重定位节:
- 符号表:
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在0x40000~0x401000段中,程序被载入,虚拟地址0x4004c0开始,到0x400f80结束,根据前文的节头部表,可以通过edb找到各个节的信息。
5.5 链接的重定位过程分析
命令:objdump -d -r hello
(1)分析:
比较发现,hello比hello.o的反汇编文件多了许多节。hello.o的反汇编文件中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000。而hello中有.init,.plt,.text三个节,而且每个节中有很多函数。程序各个节完整性有所提高,跳转地址通过重定位后也发生了改变,库函数的代码也都已经链接到了程序中。
(2)hello的重定位过程:
- 重定位节和符号定义:
链接器将所有类型相同的节合并为同一类型的新的聚合节,这个节就作为可执行目标文件的节。然后链接器将运行时内存地址赋给新的聚合节,赋给输入模块的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
- 重定位节中的符号引用:
这一步中,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。
C.重定位条目当编译器遇到对最终位置未知的目标引用时,它会生成一个重定位条目。代码的重定位条目放在.rel.txt中。
5.6 hello的执行流程
(1)开始执行:_start、_libc_start_main;
(2)执行main:_main、_printf、_exit、_sleep、_getchar;
(3)退出:exit;
程序名 | 程序地址 |
_start | 0x400550 |
_libc_start_main | 0x600ff0 |
main | 0x400582 |
_printf | 0x400500 |
_exit | 0x400530 |
_sleep | 0x400540 |
_getchar | 0x400510 |
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序。使用共享库函数时,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
由helloELF文件可知,.got.plt起始表的位置为0x601000。
GOT表调用dl_init前0x601000后的16个字节均为0,调用之后条目发生变化。
5.8 本章小结
本章主要介绍了链接的概念及作用,以及生成链接的命令,对hello的elf格式文件进行了深入的分析,同时也分析了hello的虚拟地址空间,重定位过程,遍历了整个hello的执行过程,并且比较了hello.o的反汇编和hello的反汇编。对于链接有了更加深入的理解。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念:
进程是一个独立的可调度的任务,进程是一个抽象实体,当系统在执行某个程序时,分配和释放的各种资源。进程是一个程序的一次执行的过程。
6.1.2进程的作用:
它提供一个假象,就好像我们的程序是系统中当前运行的唯一程序一样。我们的程序好像是独占地使用内存和处理器,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
它是一种交互型的应用级程序,是Linux的外壳,提供了一个界面,它代表用户运行其他程序。
6.2.2 shell-bash的处理流程:
(1)从终端读入输入的命令。
(2)将输入字符串切分获得所有的参数。
(3)检查第一个命令行参数是否是一个内置的shell命令,如果是则立即执行。
(4)如果不是内部命令,调用fork( )创建新进程/子进程,再调用execve()执行指定程序。
6.3 Hello的fork进程创建过程
在终端中输入./hello命令后,shell会处理该命令,并判断是否为shell中的内置命令,判断出不是内置命令后,调用fork( )函数创建一个新的子进程,子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。
6.4 Hello的execve过程
当调用fork()函数创建了一个子进程之后,子进程将会调用execve( )函数,来运行hello程序,具体过程如下:
(1)删除之前进程在用户部分已存在的结构。然后创建一组新的代码、数据、堆栈段,并初始化为0;
(2)映射私有区域和共享区域,将新的代码和数据段初始化为可执行文件中的内容;
(3)设置程序计数器(PC),使它指向代码区域的入口点。
6.5 Hello的进程执行
hello程序的执行是依赖于进程所提供的抽象的基础上,进程提供给应用程序的抽象有:
(1)一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器;
(2)一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。
6.5.1逻辑控制流:
如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。
6.5.2 时间片:
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.5.3用户模式和内核模式:
Shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。
6.5.3上下文切换
如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。是一种比较高层次的异常控制流。
6.5.4调度
在对进程进行调度的过程,操作系统主要做了两件事:加载保存的寄存器,切换虚拟地址空间。
6.5.5用户态与核心态转换
划分用户态与核心态是为了能让处理器安全运行,限制应用程序可执行指令所能访问的地址范围。其中核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
6.6 hello的异常与信号处理
6.6.1 异常种类
(1)中断:来自I/O设备的信号,异步,总是返回下一条指令。
(2)陷阱:有意的异常,是执行一条指令的结果,同步,调用后也会返回到下一条指令,用来调用内核的服务进行操作。
(3)故障:错误引起,可能被故障处理程序修正,同步,如果修正成功,则将控制返回到当前指令,否则将终止程序。
(4)终止:不可恢复的致命错误,同步,直接终止应用程序。
6.6.2 异常信号
SIGINT,SIGSTP,SIGCONT,SIGHLD等
6.6.3运行结果
- 正常运行,程序结束后被正常收回
(2)按下Ctrl+Z,给进程发送SIGSTP信号,hello程序将被挂起,用ps命令可以看到hello进程并没有回收
- 按下Ctrl+C会给进程发送SIGINT信号,程序将被终止回收
- 不停乱按
6.7本章小结
本章对进程的概念及其作用,shell的功能和处理流程进行了介绍,并详细分析了hello程序从fork进程的创建,到execve函数执行,最后具体执行过程以及出现异常的处理。对于整个进程管理有了更加深入的理解。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
(1)预处理:hello.c预处理生成文本文件hello.i;
(2)编译:hello.i编译生成汇编文件hello.s;
(3)汇编:hello.s汇编生成二进制可重定位目标文件hello.o;
(4)链接:hello.o链接生成可执行文件hello;
(5)创建子进程:bash进程调用fork( )函数,生成子进程;
(6)加载程序:调用execve( )函数加载运行当前进程的上下文,并运行新程序hello;
(7)访问内存:hello的运行需要地址的概念,虚拟地址是计算机系统最伟大的抽象;
(8)交互:hello的输入输出与外界交互,与linux I/O息息相关;
(9)终止:hello最终被shell父进程回收,内核会收回为其创建的所有信息。
文件名称 功能
hello.c 源程序
hello.i 修改了的源文件(文本文件)
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位目标执行文件
hello 链接后的可执行文件
hello.elf hello.o的ELF格式
hello1.txt hello.o的反汇编
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.