计算机系统基础——程序人生-Hello’s P2P

 

计算机系统基础

——程序人生-Hello’s P2P          

 

摘  要

Hello world的一生由hello.c文件开始,经过被gcc整合的功能块cpp(预处理器),ccl(编译器),as(汇编器)之后变为可重定位的目标文件,再经由ld(链接器)的符号解析和重定位之后成功变为可执行目标文件。本文通过分析一个hello.c的完整的生命周期,从它开始被编译,到被汇编、链接、在进程中运行,讲解了Linux计算机系统执行一个程序的完整过程。

 

关键词:预处理, 编译, 汇编, 链接, 进程, 虚拟内存                         

 

目  录

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

5.3 可执行目标文件hello的格式

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.2 Intel逻辑地址到线性地址的变换-段式管理

7.3 Hello的线性地址到物理地址的变换-页式管理

7.4 TLB与四级页表支持下的VA到PA的变换

7.5 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

7.8 缺页故障与缺页中断处理

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献





第1章 概述

1.1 Hello简介

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

1.2 环境与工具

硬件环境

X64 CPU;2GHz;4G RAM;256GHD Disk

软件环境

Windows10 64位;VMware 14;Ubuntu 18.04

开发工具

Visual Studio 2017 64位;CodeBlocks;vim,gpedit+gcc,as,ld,edb,readelf,HexEdit

1.3 中间结果

hello.i —— 修改了的源程序(文本)

 

hello.s —— 汇编程序(文本)

 

hello.o —— 可重定位目标程序(二进制)

 

hello —— 可执行目标程序(二进制)

1.4 本章小结

本章简要的概括了hello world一生的两个阶段:P2P与020的过程,以及进行实验时的软硬件环境及开发与调试工具,以及在本论文中生成的中间结果文件。

 

 

 

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

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

2.1.2预处理的作用

预处理主要体现在宏定义、文件包含、条件编译三个方面,预处理命令以符号“#”开头。预处理会导入宏定义与文件、头文件中的内容,使得程序能完整、正常的运行,预处理生成了hello.i的源代码文本文件。

2.2在Ubuntu下预处理的命令

通过输入gcc hello.c -E -o hello.i可以对hello.c进行预处理,得到hello.i

2.3 Hello的预处理结果解析

 

在预处理文本文件hello.i中,首先是对文件包含中系统头文件的寻址和解析

hello.i文件可以看到,它的前面头文件等等被展开了,变成了很多以#开头的内容,在原有代码的基础上,将头文件stdio.h的内容引入,例如声明函数、定义结构体、定义变量、定义宏等内容。hello.i的最下面是我们熟悉的C语言程序。

 

2.4 本章小结

本章通过了解预处理的概念及作用,进行Ubuntu下预处理操作,并讲述了编译器的工作,以及我们怎样在Ubuntu下将一个与处理文件变为一个汇编代码文件,解释了在汇编代码中是如何实现c语言中的各项数据和指令的。

 

第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译过程就是将预处理后得到的预处理文件进行词法分析、语法分析、语义分析、优化后,生成汇编代码文件,本文中是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序的过程,生成一个hello.s的汇编语言源程序文件。

3.1.2编译的概念

编译的作用是将高级语言转变为更易于计算机读懂的汇编语言,同时它还可以进行语法检查、程序优化。

3.2 在Ubuntu下编译的命令

Ubuntu终端下进入hello.c所在文件,输入指令gcc hello.i -S -o hello.s,按下回车即可。

 

3.3 Hello的编译结果解析

 

得到的hello.s就是编译后得到的汇编语言源程序文件,通过gedit查看它,发现它的头部声明了全局变量和它们存放的节段,接下来是将源程序的命令汇编得到的代码,接下来对它们进行分析。

 

3.3.1 全局变量

在hello.c中有一个全局变量sleepsecs,它被定义成int型,但在编译器编译的过程中将它优化为了long型,这里编译器进行了隐式的类型转换,我们给它赋值为2.5,查看sleepsecs的值时发现sleepsecs = 2,它被存放在.rotate节中。

 

3.3.2 局部变量

通过观察这里,我们发现在.L2中声明了一个局部变量i,将其存储在-4(%rbp)中,可以得知在处理局部变量时,编译到当前位置才去申请这样一个内存空间的。

 

3.3.3 赋值

程序中涉及的赋值操作有:

int sleepsecs=2.5 :因为sleepsecs是全局变量,所以直接在.data节中将sleepsecs声明为值2的long类型数据。

i=0:整型数据的赋值使用mov指令完成,根据数据的大小不同使用不同后缀,分别为:

指令      b           w               l              q

大小      8b (1B)      16b (2B)      32b (4B)      64b (8B)

因为i是4B的int类型,所以使用movl进行赋值。

                    

3.3.4 类型转换

程序中涉及隐式类型转换的是:int sleepsecs=2.5,将浮点数类型的2.5转换为int类型。当在double或float向int进行类型转换的时候,程序改变数值和位模式的原则是:值会向零舍入。例如1.999将被转换成1,-1.999将被转换成-1。进一步来讲,可能会产生值溢出的情况,与Intel兼容的微处理器指定位模式[10…000]为整数不确定值,一个浮点数到整数的转换,如果不能为该浮点数找到一个合适的整数近似值,就会产生一个整数不确定值。

浮点数默认类型为double,所以上述强制转化是double强制转化为int类型。遵从向零舍入的原则,将2.5舍入为2。

3.3.5 算术操作

汇编语言中加减乘除四则运算是通过语句来实现的:

指令效果

leaq S,D      D=&S

INC D      D+=1

DEC D      D-=1

NEG D      D=-D

ADD S,D      D=D+S

SUB S,D      D=D-S

IMULQ S      R[%rdx]:R[%rax]=SR[%rax](有符号)

MULQ S      R[%rdx]:R[%rax]=SR[%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

 

i++,对计数器i自增,使用程序指令addl,后缀l代表操作数是一个4B大小的数据。

汇编中使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi。

 

3.3.6 关系操作

关系操作就是比较两个变量的大小情况,通过cmpl来执行,在cmpl中比较两个数的大小,用后一个数减去前一个数得到结果的情况来设置标志位,接下来可以通过设置的标志位进行跳转等操作。

 

3.3.7 控制转移之条件语句

通过cmpl进行比较,根据比较的结果通过jx进行跳转,跳转方式可以通过查看跳转表得到

 

3.3.8 控制转移之循环语句

这里就是一个循环语句的开始,可以发现我们的循环条件是i < 10,在这里被优化为了i <= 9,每次将计数器的值与9进行比较,若小于等于则跳转到循环内部L4执行.

循环内部语句如下,在每次执行.L4结束后都将-4(%rbp)加1,因此它是i,起到一个计数器的作用。

 

3.3.9 函数操作

参数传递:在函数的参数传递中使用不同的寄存器来保存第x个参数

函数调用:使用call语句来实现函数的调用。

函数返回:函数的返回值保存在%rax中,将需要返回的变量值存在%rax中,在进行函数的操作之后ret即可返回%rax中的值。

 

程序中涉及函数操作的有:

main函数:

传递控制,main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址dest压栈,然后跳转到main函数。

传递数据,外部调用过程向main函数传递参数argc和argv,分别使用%rdi和%rsi存储,函数正常出口为return 0,将%eax设置0返回。

分配和释放内存,使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP,将下一条要执行指令的地址设置为dest。

printf函数:

传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。

控制传递:第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf使用call printf@PLT。

exit函数:

传递数据:将%edi设置为1。

控制传递:call exit@PLT。

sleep函数:

传递数据:将%edi设置为sleepsecs。

控制传递:call sleep@PLT。

getchar函数:

控制传递:call gethcar@PLT

3.4 本章小结

本阶段完成了对hello.i的编译工作。使用编译指令可以将其转换为.s汇编语言文件。完成该阶段转换后,可以进行下一阶段的汇编处理。

 

第4章 汇编

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值