本大作业Github地址
计算机系统大作业
题 目 程序人生-Hello’s P2P
学 号 1183710113
学 生 许健
指 导 教 师 史先俊
计算机科学与技术学院
2019年12月
摘 要
对于每个程序员来说,Hello World是一个开始,本论文目的在于利用gcc、edb等工具,结合CSAPP教材,研究hello程序在Linux系统下的整个生命周期,从而达到融会贯通所学知识的效果。
关键词: CSAPP;HIT;大作业;hello
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
第1章 概述
1.1 Hello简介
P2P:程序员用键盘输入hello.c文件,一个hello的C语言文件诞生,然后经过预处理器、汇编器、编译器、链接器的一系列处理,hello可执行文件诞生了,在shell中键入启动命令后,shell为其fork,产生子进程,于是hello便从Program变成了Process。
020:shell调用execve函数在新的子进程中加载并运行hello,在hello运行的过程中,还需要CPU为hello分配内存、时间片,使得hello看似独享CPU资源。系统的进程管理帮助hello切换上下文、shell的信号处理程序使得hello在运行过程中可以处理各种信号,当程序员主动地按下Ctrl+Z或者hello运行到return 0时,hello所在进程将被杀死,shell会回收它的僵死进程,内核删除相关数据结构。
1.2 环境与工具
硬件环境:Inter® Core™ i5-7300HQ CPU;2.5GHz;8G RAM;128G SSD+1T HDD
软件环境:Windows 10 64位;Vmware 15;Ubuntu 19.04 64位
开发与调试工具:gcc;edb; readelf;objdump;gedit;hexedit;
1.3 中间结果
hello.c——原文件
hello.i——预处理之后文本文件
hello.s——编译之后的汇编文件
hello.o——汇编之后的可重定位目标执行
hello——链接之后的可执行目标文件
hello.elf——hello.o的elf格式,用来看hello.o的各节信息
hello.ob——hello.o的反汇编文件,用来看汇编器翻译后的汇编代码
hello1.ob——hello的反汇编文件,用来看链接器链接后的汇编代码
1.4 本章小结
本章主要简单介绍了hello的P2P,020过程,列出了本次实验信息:环境、中间结果。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。将所引用的所有库展开,处理所有的条件编译,并执行所有的宏定义,得到另一个通常是以.i作为文件扩展名的C程序。
预处理的作用:
-
将c程序中所有#include声明的头文件复制到新的程序中。比如hello.c中第6~8行的#include <stdio.h>、#include <unistd.h>、#include <stdlib.h>命令告诉预处理器读取系统头文件stdio.h、unistd.h、stdlib.h的内容,并把它直接插人程序文本中;
-
条件编译。根据条件#if决定是否处理之后的代码;
-
执行宏替换。用实际值替换用#define定义的字符串。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
图2.1 对hello.c进行预处理
2.3 Hello的预处理结果解析
使用Text Editor打开hello.i,发现原来的helloc.c已经被拓展成了3042行,前面的内容是hello.c的三个#include指令包含的头文件的代码,先寻找main函数,main函数从第3029行开始,如下图。
图2.2 hello.i中的main函数
再看之前的头文件的处理,以第一条#include指令为例,cpp到默认的环境变量下搜索stdio.h头文件,打开/usr/include/stdio.h,发现其中仍有#include指令,于是再去搜索包含的头文件,直到最后的文件中没有#include指令,并把所有文件中的所有#define和#ifdef指令进行处理,执行宏替换和通过条件确定是否处理定义的指令。如图是对stdio.h包含文件的展开。
图2.3 #include<stdio.h>包含文件展开
2.4 本章小结
本章主要介绍了预处理的概念及作用,并结合hello.c处理后的hello.i对处理过程进行分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器将文本文件hello.i翻译成另一个文本文件hello.s,它包含一个汇编语言程序。
编译的作用:将字符串转化成内部的表示结构,然后得到一系列记号,生成语法树,最后将语法树转化为目标代码。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图3.1 hello.i编译生成hello.s
3.3 Hello的编译结果解析
3.3.1 数据
- 字符串
程序中用到的字符串有:“用法: Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”。编译器一般将字符串存放在.rodata节,这两个个字符串在hello.s中的存储如下图,可以看到第一个字符串中的汉字被编码成UTF-8格式,一个汉字占三个字节,每个字节用\分隔。第二个字符串中的两个%s为用户在终端运行hello时输入的两个参数。
图3.2 hello.s存储的两个字符串
- 整数
hello.c中的整型变量有argc和i。
其中argc是从终端传入的参数个数,也是main函数的第一个参数,所以由寄存器%edi进行保存。由图3.3的21行可知,argc又被存入了栈中-20(%rbp)的位置。
图3.3 argc被保存在栈中
i则是局部变量,用来控制循环次数的计数器,编译器会将局部变量保存在寄存器或者栈中,由图3.4的30行看出hello.s将i存储在栈中-4(%rbp)的位置。
图3.4 .L2中的i的位置
- 数组
hello.c中数组是main函数的第二个参数,char *argv[],是字符指针数组,由于是第二个参数因而被保存在寄存器%rsi中,由图3.5的第22行可知它随后又被保存在了栈中-32(%rbp)的位置。
图3.5 argv被保存在栈中
在访问argv[]所指向的内容时,每次先获得数组的起始地址,如图3.6的第33、36、43行,然后通过加8*i来访问之后的字符指针,如图3.6中的第34、37、44,原因是每个字符指针所占的空间大小围为8个字节。然后通过获得的字符指针寻找字符串,如图3.6中的第35、38、45行。
图3.6 访问argv数组元素
3.3.2 赋值
hello.c中的赋值操作只有i=0这一条,这条语句在汇编中用mov指令实现,由于int占4个字节,所以以‘l’作为后缀。如图3.7中的第30行。
图3.7 给i赋值
3.3.3 类型转换
程序中涉及的类型转换只有一处,如图3.8所示的第19行,使用atoi函数将命令行的第三个字符串参数转换成了整型。
图3.8 hello.c的main函数
3.3.4 算术操作
汇编语言中有如下几种算术操作:
指令 | 行为 | 描述 |
---|---|---|
inc D | D=D+1 | 加1 |
dec D | D=D-1 | 减1 |
neg D | D=-D | 取反 |
add S,G | D=D+S | D加S |
sub S,D | D=D-S | D减S |
imul S,D | D=D*S | D乘S |
imulq S | R[%rdx]:R[%rax]=S*R[%rax] | 有符号乘法 |
mulq S | R[%rdx]:R[%rax]=S*R[%rax] | 无符号乘法 |
idivq S | R[%rdx]=R[%rdx]:R[%rax] mod S R[%rax]=R[%rdx]:R[%rax] div S | 有符号除法 |
divq S | R[%rdx]=R[%rdx]:R[%rax] mod S R[%rax]=R[%rdx]:R[%rax] div S | 无符号触发 |
leaq S,D | D = &S | 加载有效地址 |
helo.c中的算术操作只有一处,循环变量i的自增运算,在hello.s中处理成如图3.9的形式。
图3.9 i的自增运算
3.3.5 关系操作
C语言中的关系操作有==、!=、>、<、>=、<=,这些操作在汇编语言中主要依赖于cmp和test指令实现,cmp指令根据两个操作数之差来设置条件码。cmp指令与SUB指令的行为是一样,而test指令的行为与and指令一样,除了它们只设置条件码而不改变目的寄存器的值。
在hello.c中有两处用到了关系操作,分别是图3.8中的第13行的argc!=4和第17行的i<8。这两句在hello.s中被分别处理为图3.10和图3.11的形式。cmp之后设置条件码,为之后的je和jle提供判断依据。
图3.10 argc!=4在hello.s中的体现
图3.11 i<8在hello.s中的体现
3.3.6 数组/指针/结构操作
在hello.c中通过下标访问argv数组,在hello.s中访问argv[1]的操作如图3.12所示,第36行是取argv首地址,第37行是通过首地址加8字节找到argv[1]的地址,第38行是通过argv[1]中的内容找到对应的字符串,保存在寄存器%rax中。对argv数组其他元素所指的字符串也同理。
图3.12 访问argv[1]所指的字符串
3.3.7 控制转移
程序涉及到的控制转移有两处。
第一处是判断argc是否与4相等,在hello.s中如图3.13所示,第23行cmpl比较argc和4设置条件码之后,第24行通过判断条件码ZF位是否为零决定是否跳转到.L2,如果为0,说明argc等于4,代码跳转到.L2继续执行,如果不为0,则执行图中第25行的指令。
图3.13 对if语句的处理
第二处是判断循环变量i是否满足循环条件i<8。如图3.14所示,在第30行循环变量i被初始化为0,第30行无条件跳转到.L3,进入循环判断,在52行cmpl比较i和7之后设置条件码,然后第53行判断是否满足i<=7的要求,如果满足,跳转到.L4执行循环体,如果不满足,则退出循环,执行第54行的指令。
图3.14 对for循环的处理
3.3.8 函数操作
函数是一种过程,提供了一种封装代码的方式。P调用Q时有如下行为:
传递控制:开始执行Q的时候,PC必须设置为Q的代码的起始地址,而在返回时要把PC设置为P中调用Q之后一条语句的地址。
传递数据:P能够向Q传递任意个数的参数,Q能够向P返回0或1个值。P向Q传递参数时,64为程序参数存储顺序如下表:
第一个 | 第二个 | 第三个 | 第四个 | 第五个 | 第六个 | 第七个及之后 |
---|---|---|---|---|---|---|
%rdi | %rsi | %rdx | %rcx | %r8 | %r9 | 栈中 | </