HIT计算机系统CSAPP大作业
摘 要
本文从普通的hello程序出发,展示了hello从.c文件如何通过预处理、编译、汇编、链接阶段转变为可执行文件的全过程。还介绍了hello进程在shell执行的过程,存储管理的过程,I/O处理的过程这些运行机制。
关键词:计算机系统;P2P;O2O;
一、第1章 概述
1.1 Hello简介
·P2P过程
P2P也就是From Program to Process的过程,也就是从程序到进程的过程。 Hello文件在经过以下四个阶段的流程后,就成为了我们所需要的目标可执行 文件。
1.预处理:C预处理器扩展源代码,插入所有用#include命令指定的文件, 并扩展所有用#define声明指定的宏。
2.编译:编译器产生两个源文件的汇编代码。
3.汇编:汇编器会将汇编代码转化成二进制目标代码文件。
4.链接:链接器最后将目标代码文件与实现库函数的代码合并,产生最终 的可执行代码文件。
·020过程
020也就是From Zero-0 to Zero-0的过程。
shell首先fork一个子进程,然后通过execve加载并执行Hello,映射虚拟内存,进入程序入口后将程序载入物理内存,进入 main函数执行目标代码,CPU为运行的Hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收Hello进程,内核删除相关数据结构。即,从0开始,以0结束,为020。
1.2 环境与工具
1.2.1 硬件环境
·X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
·Windows10 64位;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上
1.2.3 开发工具
·Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
1.hello-------------------链接后的可执行文件
2.hello.c-----------------hello的源文件
3.hello.elf---------------hello的elf文件
4.hello.i------------------hello.c经过预处理后的文件
5.hello.o-----------------hello.s汇编后的可重定位目标文件
6.hello.objdump-------hello可执行文件的反汇编
7.hello.s-----------------hello.i编译后的汇编文件
8.hello_o.objdump----hello.o的反汇编
9.hello_o_elf.txt-------hello.o的ELF文件
1.4 本章小结
本章对hello进行了简单的介绍,分析了其P2P和020的过程,也列出了本次任务的硬件、软件环境和调试工具,并且列举了任务过程中出现的中间产物,是后续实验部分的基础。
二、第2章 预处理
2.1 预处理的概念与作用
·概念
预处理是预处理器cpp对一个程序处理的第一步,修改原始的C程序,将.c文件进行初步处理成一个.i文件。操作的对象为原始代码中以字符#开头的命令,包括#include的头文件、#define的宏定义,#if、#ifdef、#endif等条件编译。
·作用
1.预处理器会读取头文件中用到的库的代码,将这段代码直接插入到程序文件中。
2.会删除“#define”并展开所定义的宏,完成宏替换。
3.删除所有注释。
4.添加行号和文件名标识,以便编译时编译器产生调试用的行号信息。
预处理阶段的作用是让编译器在随后对文本进行编译的过程中,更加方便,因为访问库函数这类操作在预处理阶段已经完成,减少了编译器的工作。
2.2在Ubuntu下预处理的命令
·在命令行输入:
gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
·针对如下三条语句的预处理
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
2.4 本章小结
本章主要介绍了预处理的定义与作用、并结合预处理之后的程序对预处理结果进行了解析。预处理是计算机对程序进行操作的第一个步骤,在这个过程中预处理器会对.c文件进行初步的解释,对头文件、宏定义和注释进行操作,最后将初步处理完成的文本保存在.i文件中。
三、第3章 编译
3.1 编译的概念与作用
·概念
编译阶段是编译器对.i文件进行处理的过程。此阶段编译器会完成一系列对代码的语法和语义的分析,生成汇编代码,并保存在.s文件中。
·作用
1.语法检查:检查源程序是否合乎语法。如果不符合语法,编译程序要指出语法错误的部位、性质和有关信息。编译程序应使用户一次上机,能够尽可能多地查出错误。
2.调试措施:检查源程序是否合乎设计者的意图。为此,要求编译程序在编译出的目标程序中安置一些输出指令,以便在目标程序运行时能输出程序动态执行情况的信息,如变量值的更改、程序执行时所经历的线路等。这些信息有助于用户核实和验证源程序是否表达了算法要求。
3.修改手段:为用户提供简便的修改源程序的手段。编译程序通常要提供批量修改手段(用于修改数量较大或临时不易修改的错误)和现场修改手段(用于运行时修改数量较少、临时易改的错误)。
3.2 在Ubuntu下编译的命令
·在命令行输入:
gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
·汇编指令
.file -------------- C文件声明
.text -------------- 代码段
.globl ------- -----声明全局变量
.data -------------已初始化的全局和静态C变量
.align 4 ----------声明对指令或者数据的存放地址进行对齐的方式
.type -------- -----指明函数类型或对象类型
.size -------------- 声明变量大小
.long .string----- 声明long型、string型数据
.section .rodata-- 只读数据段
·数据解析
1.整数
1.1 int argc
%edi保存传入函数的第一个参数,即argc。
1.2 int i
编译器将局部变量存储在寄存器或者栈空间中,在hello.s中编译器将i存 储在%rbp-4中,图3-5中可以看出i占据了栈中的4位,符合int类型。
2.数组
2.1 char argv[]
argv单个元素char大小为8位,argv指针指向已经分配好的、一片存放 着字符指针的连续空间,起始地址为argv。图3-6分析了argv传入函数时存储 的位置。
%rsi保存传入函数的第二个参数,即数组argv的首地址。
3.字符串
3.1 “Usage: Hello 学号 姓名!\n”
第一个printf传入的输出格式化参数,存放在只读数据段.rodata中,可以 发现字符串被编码成utf-8格式,一个汉字在utf-8编码中占三个字节,一个\ 代表一个字节。如图3-7所示。
3.2 “Hello %s %s\n”
第二个printf传入的输出格式化参数,存放在只读数据段.rodata中。如图 3-8所示。
·赋值
1.int i
i是保存在栈中的局部变量,使用mov语句并以l为后缀构成movl对i进 行赋值。
·算数操作
1.for循环中i++
采用add指令,用addl对i进行增加。
2.leaq计算LC1段地址
for循环内部需要对LC1处字符串进行打印,使用了加载有效地址指令leaq 计算LC1的段地址%rip+.LC1并传递给%rdi。
·控制转移
1.if (argv!=4)
对于if判断,编译器使用 跳转指令实现,首先使用cmpl $4, -20(%rbp),设置条件码,使用je判断ZF 标志位,如果为0,说明argv-4=0, 即argv==4,则直接跳 转到.L2,否则顺 序执行下一条语句,即执行if中 的代码。
2.for(i=0;i<8;i++)
首先跳转到位于循环体.L4之后的比较代码,使用cmpl进行比较,如果 i<=7,则跳入.L4 for循环体执行,否则说明 循环结束,顺序执行for之后的逻 辑。
·数组操作
C源程序中的数组操作出现在循环体for循环中,每次循环中都要访问 argv[1]、argv[2]这两个内存。
argv[1]:数组首地址存放于-32(%rbp),先将其存储到%rax中,再加上 偏移量$8,再将该位置内容放在%rsi中,成为下一个函数的第一个参数。
argv[2]:数组首地址存放于-32(%rbp),先将其存储到%rax中,再加上 偏移量$24,再将该位置内容放在%rdi中,成为下一个函数的第二个参数。
·函数操作
函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和 可选的返回值实现某种功能。
hello.c中涉及的函数操作有:
1.main函数:
(1)传递控制:
系统启动函数调用,call指令将下一条指令的地址压栈,然后跳转到 main函数。
(2)传递数据:
外部调用过程向main函数传递参数argc和argv,分别使用%edi和%rsi 存储,函数正常出口为return 0,将%eax设置0返回。
(3)分配和释放内存:
使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时, 调用leave指令,leave相当于
mov %rbp,%rsp
pop %rbp
恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP。
2.printf函数:
(1)传递数据:
第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。 第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rdx为argv[1],%rsi 为argv[2]。
(2)控制传递:
第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf 使用call printf@PLT。
3.exit函数:
(1)传递数据
将%edi设置为1。
(2)控制传递
call exit@PLT。
4.sleep函数:
(1)参数传递:
将atoi的返回值%eax通过%rdi传递给sleep函数
(2)控制传递:
调用了sleep函数,将控制传送。
(3)函数返回:
从sleep中返回。
5.getchar函数:
(1)控制传递
call gethcar@PLT
6.atoi函数
(1)参数传递:将argv[3](字符串)通过%rdi传递给atoi函数。
(2)控制传递:通过call atoi@PLT函数,进行函数调用。
(3)函数返回:从atoi中返回。
3.4 本章小结
·本章主要介绍了有关编译的概念作用,然后使用gcc -S hello.c -