
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L020413
班 级 2003004
学 生 范致远
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
摘 要
本文以hello的自白为出发点,依次介绍而分析了hello,c文件预处理、编译、汇编和链接生成可执行文件的全过程。并在生成可执行文件后进一步分析hello进程的管理、储存管理以及IO管理。通过对hello程序一生的追踪,对计算机系统的知识有更加深入的理解。
关键词:hello;预处理;编译;汇编;链接;进程管理;储存管理;IO管理。
目录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:From Program to Process,即从程序到进程。hello.c文件经过预处理、编译、汇编、链接四个过程,生成可执行文件hello,并在命令行执行后,通过调用fork函数创建子进程执行hello,使得其变成了一个进程。
020:From Zero-0 to Zero-0,即从无到有,再从有到无。产生hello的子进程后,通过调用execve加载hello,映射虚拟内存,并将其载入物理内存。之后开始执行hello程序的相关代码。当进程执行结束后,父进程将回收该进程,并且内核删除其相关的数据结构。hello便结束了其一生。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows11 64位;VirtualBox/Vmware 16;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上
1.2.3 开发工具
Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
hello.c——源文件
hello.i——hello.c经过预处理生成的文件
hello.s——hello.i经过编译生成的文件
hello.o——hello.s经过汇编生成的文件
hello——hello.o经过汇编生成的可执行文件
hello.o.elf——hello.o的elf文件
hello.o.objdump——hello.o的反汇编文件
hello.elf——hello的elf文件
hello.objdump——hello的反汇编文件
1.4 本章小结
本章中简述了hello的P2P和020的整个过程,将本次实验过程中所处的软硬件环境和开发与调试工具列了出来,同时也将整个实验过程中生成的中间文件名称及作用做了说明,是对本次实验主要内容的一个概括。
第2章 预处理
2.1 预处理的概念与作用
概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理器(CPP)根据以字符#开头的命令,修改原始的C程序。结果结果就得到了另一个C程序,通常是以.i作为文件扩展名。
作用:
①文件包含:#include 。将文件内容插入到程序中。如hello.c中的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。
②宏定义:#define 。根据一系列预定义的规则替换一定的文本模式。如#defined N 200,那么程序中就会将所有的N换成200。
③条件编译:#if/#ifdef/#ifndef/#else/#elif/#endif 。通过在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃。
④行控制:#line 。
⑤错误指令:#error 。
⑥和现实相关的杂注:#pragma 。
⑦空指令:# 。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i

图1.预处理命令

图2.预处理生成文件
2.3 Hello的预处理结果解析
可以看到,预处理产生的文件与hello.c文件相比,在程序之前多了许多的内容,这主要是对原文件中三个#include语句进行预处理的结果,将其头文件所包含的文件插入到了程序文本中。

图3.hello.i文件
2.4 本章小结
在本章中本章主要对预处理的概念、作用、过程和结果做了详细的介绍。同时,以hello.c转变为hello.i这个预处理过程为例,具体的观察分析了程序进行预处理的过程。总的来说,预处理过程中,对.c文件进行文件包含、宏定义、条件编译等的预处理,并最终生成了.i文件。
第3章 编译
3.1 编译的概念与作用
概念:编译器(cc1)将源程序的每一条语句都编译成汇编语言,并保存成二进制文件的过程。
作用:对程序的编译经过词法分析、语法分析、代码优化等一系列过程最终完成,并生成最终的二进制文件。在这个过程中,编译器可以实现对程序编写的纠错和优化等,并最终将程序代码翻译成计算机可以读懂的汇编语言,使得计算机可以直接执行。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s

图4.编译命令

图5.生成文件
3.3 Hello的编译结果解析
3.3.1数据
3.3.1.1常量
在本程序中,只存在字符串常量,即下图语句:
![]()
图6.语句
在hello.s文件中,该字符串常量被转换成编码来进行表示,如下图所示:

图7.字符串
3.3.1.2整型变量
程序中存在两个整型变量,且均为局部变量。一个为函数参数argc,一个为函数内定义的i。
这两个变量均为局部变量,所以储存在栈中,不同的是,由于argc为函数参数,所以刚开始时会放在寄存器中传给函数,之后存放在栈中。
可以看到,argc作为第一个参数通过寄存器edi传递进来,并储存在了栈里。
![]()
图8.第一个参数
同样的,i也被存放在栈里,并被赋初值0.
![]()
图9.第二个参数
3.3.1.3字符串数组
该字符字符串数组通过指针的形式作为函数的参数传递进来,那么同样的,它的起始地址也是起初保存在寄存器中,后又被转存到栈中,如下图语句所示:
![]()
图10.字符串数组
而在下文对数组内容进行输出时,通过将字符串数组内容保存到寄存器rsi和rdx后,传递给printf函数,实现对内容的输出。

图11.字符串输出

图12.字符串输出
3.3.1.4类型转换
在原程序中,通过‘atoi(argv[3])’将字符串类型的argv[3]转换成了整型。在编译后的文件中,首先将字符串转存到寄存器rdi中,之后将其作为一个参数,调用atoi实现类型的转换。
![]()
图13.类型转换
3.3.2赋值
原文件中只涉及对变量i的赋值操作,将其赋值为0。在编译后的文件中,通过movl指令将0赋给i,由于i是整型,四个字节,所以采用l后缀。
![]()
图14.赋值
3.3.3算数操作
程序中涉及的算数操作即为每次循环变量i的i++。在编译后的文件中,采用addl指令实现对变量i的++操作。同样的,由于i是整型,所以采用l后缀。
![]()
图15.算数操作
3.3.4关系操作
3.3.4.1 “!=”
在if语句中,进行了argc!=4的操作,在编译后的文件中是通过cmpl和je指令合作实现的。如下图,首先通过cmpl指令进行比较,在通过je控制跳转来实现argc!=4的判断,je即若相等则跳转到.L2处,这样就实现了两数不等的比较。
![]()
图16.“!=”
3.3.4.2 “<”
在for循环语句中,控制退出的条件是i<8,对于<的实现,编译后的文件是通过cmpl和jle来实现的。如下图,通过cmpl进行两数比较,jle判断是否小于等于,若i小于等于7则跳到.L4,大于7执行另外的语句,这样就完成了i<8的关系操作。
![]()
图17.“<”
3.3.5数组/指针/结构操作
程序中存在着一个字符串数组argc[],而该数组实现的过程中也采用了指针的实现形式。通过定义char* argc[]的形式实现二维的字符数组来储存字符串,数组的每一行保存一个字符串。
正如前面分析的那样,该数组首地址被储存在栈中。而对给数组的访问可以通过首地址地址加偏移量的方式实现,如程序中对数组第三行储存的字符串的访问如下,由于地址为8位,所以加上偏移量16后,获得了第三行字符串的首地址:

图18.数组
3.3.6控制转移
3.3.6.1 “if”
程序中存在一个if语句,通过判断argc是否等于4,实现两个分支,如下图,通过与4比较,若等于4,则跳转到.L2的位置,即if语句后的for循环语句,不符合if判断条件,跳过了if内的语句;而若是不等于4,则不执行跳转,继续执行下面的语句,而这就是if内的语句。

图19.if语句
3.3.6.2 “for”
程序中存在一个for循环,控制条件为i<8,其初始化如下,给i赋初值0后跳转,开始执行循环。

图20.for循环初始化
每次循环开始前,都会i与7作比较,小于等于7,即满足i<8,则跳转到.L4执行,即for循环内的语句,若i大于7了,则不再进行跳转,正常执行后面的语句,即for循环接下来的程序内容。

图21.for循环退出
3.3.7函数操作
3.3.7.1参数传递
函数的参数通过寄存器来传递,根据参数的顺序,依次用寄存器rdi,rsi,rdx,rcx,r8,r9,进行保存。如程序中的argc和argc[]:
![]()
图22.main参数
传给printf的参数,该三个参数传递的均为地址:

图23.printf参数
传给atoi的参数,字符串的首地址地址:
![]()
图24.atoi参数
传给sleep的参数,值:
![]()
图25.sleep参数
3.3.7.2函数调用
通过call指令和函数名进行调用,参数通过寄存器传递。
例如:
![]()
![]()
![]()
![]()
图26.函数调用
3.3.7.3函数返回
函数的返回值存放在寄存器rax,可以通过该寄存器获取返回值。如下图中,将atoi函数的返回值存放在edi中又作为参数传递给了sleep函数。

图27.函数返回
3.4 本章小结
编译是将我们所写的代码翻译为机器所能理解的汇编语言的过程。在本章中,介绍了编译的概念与作用。并结合hello.s的实例,详细分析了各种类型的数据、操作、函数等在汇编语言中是如何实现的。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。汇编语言是为特定计算机或计算机系列设计的一种面向机器的语言,由汇编执行指令和汇编伪指令组成。
作用:汇编器(as)将.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中。

本文详细跟踪了Hello程序从源代码hello.c到生成可执行文件的全程,包括预处理、编译、汇编和链接,深入解析了进程管理、存储管理和IO管理,揭示了计算机系统内部运作机制。
最低0.47元/天 解锁文章
1786

被折叠的 条评论
为什么被折叠?



