程序人生-Hello’s P2P
Posted 王狗王
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序人生-Hello’s P2P相关的知识,希望对你有一定的参考价值。
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术(人工智能)
学 号 7203610830
班 级 2036015
学 生 王一鸣
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文对hello程序的生命周期进行了分析,首先完成hello.c源程序的编写,之后使用C预处理器cpp对其进行预处理,生成hello.i,再运行C编译器(ccl)将其进行翻译生成汇编语言文件hello.s,然后运行汇编器(as)将其翻译成一个可重定位目标文件hello.o,最后运行链接器程序ld将hello.o和系统目标文件组合起来,创建了一个可执行目标文件hello。当shell接收到./hello的指令后开始调用fork函数创建hello进程,execve加载hello进入内存,由CPU控制程序逻辑流的运行,中断,上下文切换和异常控制流的处理,最后结束进程并由父进程进行回收,hello走向“生命”的尽头。
关键词:关键词:预处理 编译 汇编 链接 进程管理 异常与信号 虚拟内存 存储地址翻译 I/O管理
目 录
2.2在UBUNTU下预处理的命令......................................................................... - 5 -
3.2 在UBUNTU下编译的命令............................................................................ - 6 -
4.2 在UBUNTU下汇编的命令............................................................................ - 7 -
5.2 在UBUNTU下链接的命令............................................................................ - 8 -
5.3 可执行目标文件HELLO的格式.................................................................... - 8 -
6.2 简述壳SHELL-BASH的作用与处理流程.................................................. - 10 -
6.3 HELLO的FORK进程创建过程................................................................... - 10 -
6.6 HELLO的异常与信号处理............................................................................ - 10 -
7.1 HELLO的存储器地址空间............................................................................ - 11 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理............................................ - 11 -
7.3 HELLO的线性地址到物理地址的变换-页式管理....................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 11 -
7.5 三级CACHE支持下的物理内存访问......................................................... - 11 -
7.6 HELLO进程FORK时的内存映射............................................................... - 11 -
7.7 HELLO进程EXECVE时的内存映射.......................................................... - 11 -
7.8 缺页故障与缺页中断处理............................................................................. - 11 -
8.1 LINUX的IO设备管理方法.......................................................................... - 13 -
8.2 简述UNIX IO接口及其函数........................................................................ - 13 -
第1章 概述
1.1 Hello简介
hello程序的生命周期是从一个高级C语言程序开始的, 因为这种形式能够被人读懂。然而,为了在系统上运行 hello.c程序每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件,在这里,GCC编译器驱动程序读取源程序文件 hello.c, 并把它翻译成一个可执行目标文件hello。之后用户通过shell键入./hello命令开始执行hello程序,shell通过fork函数创建一个子进程,再由子进程执行execve函数加载hello。以上就是hello从源程序到一个被执行的进程的P2P(program to process)过程了。
在execve执行hello程序后,内核为hello进程映射虚拟内存。在hello进入程序入口之后,hello相关的数据被内核加载到物理内存中,hello程序开始正式被执行。内核还需要为hello分配时间片、逻辑控制流。最后,当hello程序运行结束,终止成为僵死进程后,由shell回收hello进程,在系统中删除与hello有关的数据内容。这便是hello的020(From Zero-0 to Zero-0)过程。
1.2 环境与工具
硬件环境:Intel Core i7 8750H CPU、3.0GHz、16G RAM、512G HD Disk
软件环境:Windows11 64位、Vmware15.9、Ubuntu 20.04
开发与调试工具:Codeblocks20.4、Visual Studio2022、gcc、Objdump、edb等
1.3 中间结果
文件作用 | |
hello.c | 程序的源代码 |
hello.i | hello.c文件预处理后的文本文件 |
hello.s | hello.i文件编译后的汇编文件 |
hello.o | hello.s汇编后的可重定位目标文件 |
hello | hello.s链接后的可执行文件 |
1.4 本章小结
本章简单介绍了hello的p2p,020 过程,列出了本次实验信息:环境、中间结果,并且大致简述了hello程序从c程序hello.c到可执行目标文件hello经过的历程。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理器cpp根据以字符#开头的命令,修改原始的C程序。比如hello.c 中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。或是比如对宏#define PI 3.14,就将程序中的所有PI替换成3.14。预处理的结果就得到了另一个C程序,通常是以.i作为文件扩展名。
图2.1-1 hello.c中包含的头文件
预处理的作用:
1.实现条件编译,通过预处理可以实现部分代码的在某些条件下的选择性编译。如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
2.实现宏定义,在预处理阶段用定义的实际数值将宏替换。
3.实现头文件引用,将头文件的内容复制到源程序中以实现引用。如#include "FileName"或者#include 等。该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
4.实现注释,将c文件中的注释从代码中删除。
5.实现特殊符号的使用。如处理#line、#error、#pragma以及#等。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
2.2在Ubuntu下预处理的命令
预处理命令:gcc hello.c -E -o hello.i
hello.c进行预处理结果如下:
图2.2-1 Ubuntu下的预处理命令
2.3 Hello的预处理结果解析
在运行2.2的预处理命令后,我们可以看到shell当前加载的目录下产生了新文件hello.i
图2.3-1 生成的hello.i文件
打开hello.i文件后,我们可以看到,hello.i的内容较hello.c多了很多,这是因为预处理后,包含的头文件中的内容被复制在了.i文件中,同时我们还可以发现,.c文件中的注释行被删除了。
图2.3-2 hello.c与hello.i内容对比
2.4 本章小结
介绍了预处理的相关概念及其所进行的一些处理,例如实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。展示如何在ubuntu下用gcc对.c文件进行预处理,为文件后续的操作打下基础。
第3章 编译
3.1 编译的概念与作用
编译的概念:在编译阶段,编译器检查是否有语法错误,检查无误后,编译器将.i文件翻译成.s文件,它包含一个汇编语言程序,该程序包含main函数的定义。
编译的作用:汇编语言程序为不同高级语言的不同编译器提供了通用的输出语言,为低级机器语言。编译将高级语言转化为汇编语言能够检测代码的正确性,并为接下来将汇编语言生成机器可识别的机器码做准备。
3.2 在Ubuntu下编译的命令
Ubuntu下编译的命令:gcc -S -o hello.s hello.i
图3.2-1 Ubuntu下编译的命令
3.3 Hello的编译结果解析
|
图 3.3-0‑2 hello的编译结果(2)
得到的汇编代码如上图3.3.0-1与3.3-2所示。
3.3.1初始伪指令与主函数开头
图3.3.1-1 伪指令与主函数开头的汇编语言代码
所有以’.’开头的行都是指导汇编器和链接器工作的伪指令。
.file:声明源文件
.text:代码节
.section: .rodata:只读代码段
.align 8:数据或者指令的地址对齐方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型(此处@function表示main为函数类型)
3.3.2 数据:
(1)字符串:
程序中有两个字符串,两个字符串都在只读数据段中,如上图3.3.1-1中的.LC0,.LC1所示,两个字符串作为printf的参数。字符串被放在只读数据段中。
(2)局部变量i:
Main函数声明了局部变量i,编译器进行编译时将变量放置在堆栈中,通过rbp的相对偏移来访问。
图3.3.2‑1 局部变量i的相对偏移访问
(3)形参有符号数argc、字符数组首地址*argv:
如图3.3.2-2,符号数argc和字符型数组指针argv,根据寄存器使用规则,这两个参数分别通过%edi和%rsi传递。通过rbp的偏移量来访问。在程序最开始,为main函数建立栈帧,并完成参数传递。Argc存放在-20(%rbp),argv作为main函数的参数,数组的元素都是指向字符类型的指针,起始地址存放在栈中-32(%rbp)的位置。
图3.3.2‑2 形参argc、*argv的汇编表示
(4)数组:
c代码中的数组访问有argv[1]、argv[2]、argv[3],在汇编代码中访问这三个量是通过数组首地址加偏移量的方式实现的,具体汇编代码如图3.3.2-3所示。
图3.3.2‑3数组元素访问的汇编表示
3.3.3 操作
(1)关系判断操作(条件转移):
argc!=4;是在一条件语句中的条件判断argc是否为4,进行编译时,这条指令被编译为:cmpl $4,-20(%rbp),如图3.3.3-1所示,同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。
图3.3.3‑1 argc !=4关系判断操作的汇编代码
i<8;在hello.c作为判断循环条件,被编译为:cmpl $7,-4(%rbp),如图3.3.3-2所示,计算 i与7是否相等,然后设置条件码,为下一步利用jle条件码进行跳转做准备,若不相等,则跳转到.L4,相等则运行下面的部分。
图3.3.3‑2 i<8关系判断操作的汇编代码
(2)赋值操作:
在原本c程序中的赋值操作只有i=0这个赋值语句。在汇编中是通过mov立即数的方式体现的,movb:一个字节;movw:两个字节;movl:四个字节;movq:八个字节如图3.3.3-3:
图3.3.3‑3 i=0赋值操作的汇编代码
在汇编器处理一些c语言代码的过程中也会产生一些赋值操作,还有通过以lea(地址传递)来赋值的方式,如图3.3.3-4所示:
图3.3.3‑4 lea地址传递赋值操作的汇编代码
(3)算数操作
hello.c中进行了i++的算术操作,i为int类型。在汇编代码中是通过add来实现的,如图3.3.3-5所示:
图3.3.3‑5 i++算数操作的汇编代码
(4)类型转换
Hello.c中的sleep(atoi(argv[3]));语句存在atoi()的类型转换,在hello.c中用atoi将字符串转化成int型,在hello.s中用call语句调用atoi函数强制处理该类型转换,如图3.3.-6所示。
图3.3.3‑5 atoi类型转换的汇编操作的汇编代码
(5)函数操作:
在hello.c中多处涉及函数操作,调用另一个函数来执行当前任务,如printf、atoi、getchar、sleep、exit,在hello.s中对此的处理均是先完成参数传递(有入口参数的情况下),然后在用call语句转到相应函数的入口处继续执行。
printf:%rdi传递:
图3.3.3‑6 printf函数的汇编操作的汇编代码
Exit:%edi传递:
图3.3.3‑7 exit函数的汇编操作的汇编代码
Atoi:用%rdi:
图3.3.3‑8 atoi函数的汇编操作的汇编代码
Sleep:用%edi传递:
图3.3.3‑9 sleep函数的汇编操作的汇编代码
getchar:(无传递参数)
图3.3.3‑10 getchar函数的汇编操作的汇编代码
函数返回:返回时,若有返回值,在返回之前会将返回值存放到%eax中,否则直接利用ret返回。在返回之前要恢复栈帧,删除被调用函数的栈帧,恢复调用函数的栈帧。函数返回时一个非常重要的指令为leave指令,leave指令的作用就是将被调用函数的栈帧抹除,实现过程如下图所示:
图3.3.3‑11函数返回操作的汇编代码
3.4 本章小结
本章介绍了.i文件被编译为汇编代码的过程。编译器通过词法分析和语法分析,来检查原始代码有没有错误,在确认没有错误之后,编译器会按照一定的规范生成与原始代码等价的汇编代码,在这个过程中,编译器可能会按照自己的理解,对原始代码结构和数据做出调整。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
作用:将.s文件生成.o格式机器码,使其能被链接器ld链接生成可执行文件。
4.2 在Ubuntu下汇编的命令
命令:gcc -c -o hello.o hello.s
图4.2‑1 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.3.1 ELF格式的具体内容
图4.3.1‑1 ELF格式的具体内容
4.3.2 ELF头
打开ELF 的指令:readelf -a hello.o
打开之后,可以看到ELF文件从ELF头开始,如下图4.3.2-1所示,ELF头以一个16个字节的序列开始,这个序列描述了生成该文件的系统的字大小和字节顺序。在该ELF头中显示,以小端码机器存储,文件类型为可重定位目标文件。
图4.3.2‑1 hello.o的ELF头内容
ELF头中的Magic魔数是一个定值,在程序执行时会检查魔数是否正确,如果不正确则拒绝加载。ELF头告诉了我们文件的基本信息,类别是ELF64,文件中的数据是按照2的补码储存的,小端序,文件的类型是可重定位文件,节头大小为64字节,节头的数量为14个。
4.3.3 节头表
节头表告诉了我们每个节的大小、名称、类型、读、写、执行权限以及对其方式。由于我们的程序还未进行链接,因此每个节的起始位置都是0,在链接后会为每个节进行重定位以获得起始位置。在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小,
图4.3.3‑1 节头表的内容
4.3.4 符号表
存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。同样由于还未进行链接重定位,偏移量Value还都是0。结果如下图4.3.4-1所示。
图4.3.4‑1 符号表的内容
4.3.5 重定位节
图4.3.5‑1 重定位节的内容
重定位节包含了.text文件中需要重定位的信息,在链接器将目标文件与其它文件进行链接时需要修改这些信息,可执行文件中不包含重定位节。
Offset:需要被修改的引用节的偏移Info:包括symbol和type两个部分,symbol在前面四个字节,type在后面四个字节,
symbol:标识被修改引用应该指向的符号,
type:重定位的类型
Type:告知链接器应该如何修改新的应用
Attend:一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整
Name:重定向到的目标的名称。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
图4.4‑1 hello.o反汇编的内容
反汇编代码和汇编代码在指令格式上非常相似,但在以下几个方面存在不同:
1.立即数的引用不同,在反汇编中立即数是十六进制的,而汇编代码则是十进制。
2.反汇编的语言mov、add、push后无其他字符如b、w等。
3.子程序的调用不同,反汇编代码中子程序的调用是通过对主函数地址的相对偏移进行的,而在汇编代码中则是通过call直接加上函数名的方法进行的。
4. 对全局变量的访问方式:反汇编语言通过pc相对寻址,通过rip+x的值进行访问,未重定位,故用0占位。在汇编语言中,通过.LC0+rip进行访问。
5.分支跳转不同,在反汇编代码中,分支转移是通过跳转到以主函数地址为基址的一个偏移地址中,而在汇编代码中则是通过.L4、.L3这样分块的方式来跳转的。
综上所述,反汇编代码与汇编代码在指令上是一一对应的关系,只有在一些特殊的指令需要有引用的转化,其他地方几乎完全一致。
4.5 本章小结
本章介绍了编译过程,该过程将汇编语言翻译成机器语言,以供计算机识别并执行相关指令,得到了.o文件,.o文件是可重定位目标程序,可以通过readelf来查看其信息,剖析了这些内容在接下来的链接过程中能起什么作用,并对比了汇编与反汇编代码有何异同。但是它还不是可执行程序,需要进一步的链接来生成可执行程序,即从hello.o到hello。
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是指在电子计算机程序的各模块之间传递参数和控制命令,并把它们组成一个可执行的整体的过程。链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存中并执行。链接可以执行于编译时、也就是源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时。也就是由应用程序来执行。总之是把多个文件拼接合并成一个可执行文件。
链接的作用:链接可以在编译、汇编、加载和运行时执行。链接方便了模块化编程。链接器在软件开发过程中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。
5.2 在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.2‑1 Ubuntu下进行链接的命令
5.3 可执行目标文件hello的格式
5.3.1 ELF头
图5.3.1‑1 hello.o的ELF头内容
可以看到,可执行文件的ELF头与可重定位目标文件的ELF头有以下几个不同:
1.文件的类型不同,可执行文件的类型不再是REL而是EXEC。
2.程序的入口点不一样,因为连接上了库文件,使得main函数不再是从0x0开始。同理节头的开始位置也发生了变化。
3.节头的数量产生了变化。
5.3.2 节头表
节头表对hello中所有的节信息进行了声明,其中包括大小Size以及在程序中的偏移量Offset,因此根据Section Headers中的信息我们就可以用HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。可以看到与hello.o不同,在可执行文件中经过重定位每个节的地址不再是0,而是根据自身大小加上对齐规则计算的偏移量。
5.3.3 符号表
Hello的符号表如下图5.3.3-1所示
图5.3.3‑1 hello.o的符号内容(部分)
通过观察我们可以发现,在可执行文件中多出了.dynym节。这里面存放的是通过动态链接解析出的符号,这里我们解析出的符号是程序引用的头文件中的函数。
5.3.4 重定位节
图5.3.4‑1 hello.o的重定位节内容(部分)
可以看出,由于链接了别的头文件,重定位节的偏移量与hello.o已经完全不一样了。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
根据节头表的信息,我们可以知道ELF是从0x400000开始的,如图5.4-1所示。查看edb,可以看出hello的虚拟地址空间开始于0x400000,结束于0x40ff0
图5.4.‑1 elf的虚拟地址空间
通过节头部表,找到edb的各个节的信息,如.text,虚拟地址开始于0x4010f0,字节偏移为0x10f0
图5.4.‑2 .text的虚拟地址空间
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
首先,可以看到的不同是文件中多了很多函数,一些在hello.o中没有的函数,这些都是在hello.c中没有定义却直接使用的函数,这些函数定义在共享库中,在链接时完成了符号解析和重定位,如printf、sleep等链接的时候将头文件链接到了可执行文件中。
其次,在hello.o中call、jmp指令后紧跟着的是相对地址,而hello中紧跟的是虚拟内存的确定地址,原因在于链接器完成了重定位过程,可以确定运行时的地址。在hello.o反汇编代码中出现以main加上相对偏移的跳转已经全部被重写计算,这是因为在重定位后main函数有了全新的地址,使得这个计算成为可能。同时对子函数的call引用也在重定位后重写计算来了。
程序人生-Hello’s P2P
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 120L021927
班 级 2003006
学 生 李欣桐
指 导 教 师 吴锐
计算机科学与技术学院
2021年5月
本文对hello.c程序编写成功后在Linux下如何顺利运行做出了详细的分析。hello.c源程序经过预处理、编译、汇编、链接、进程管理、存储管理、IO管理一系列操作完成了自己的生命周期。而我们可以通过一些工具清晰地观察它的一生,如gdb,edb等。
关键词:预处理;编译;汇编;链接;进程;存储;I/O;Linux
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:在编辑器中编写好程序创建hello.c源文件,经过cpp预处理生成hello.i文件,编译器ccl编译生成汇编文件hello.s,然后通过汇编器as生成二进制可重定位文件hello.o,最后链接器ld将hello.o与引用到的库函数组合起来生成二进制可执行文件hello。在shell中输入命令,shell通过fork函数创建了一个子进程,再调用execve映射虚拟内存,把程序加载到进程中,开始运行。程序运行结束时,父进程回收hello进程和它创建的子进程,操作系统内核删除相关数据结构。
020:shell为hello可执行程序进行执行,execve映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。程序运行结束后,shell回收进程,释放虚拟地址空间,删除有关进程上下文。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/ 优麒麟 64位以上
开发与调试工具:vi/vim/gedit+gcc;gdb;objdump;edb;readelf
1.3 中间结果
文件名 | 文件作用 |
hello.c | 源程序 |
hello.i | 预处理后的文件 |
hello.s | 编译后的汇编文件 |
hello.o | 汇编之后的可重定位目标文件 |
hello | 链接后的可执行目标文件 |
hello.elf | hello.o的ELF格式 |
hello1.elf | hello的ELF格式 |
hello1.txt | hello.o的反汇编 |
hello2.txt | hello的反汇编 |
1.4 本章小结
本章概括地介绍了hello的一生,介绍了P2P、020的过程,同时介绍了实验进行时使用的软硬件环境,以及开发与调试工具。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
作用:将源文件中以”include”格式包含的文件复制到编译的源文件中;用实际值替换用“#define”定义的字符串;根据“#if”后面的条件决定需要编译的代码,得到另一个C程序.i。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
图2.2.1 Ubuntu预处理的命令
图2.2.2 预处理生成hello.i文件
2.3 Hello的预处理结果解析
图2.3.1 hello.i文件部分截图
图2.3.2 hello.i文件中main函数部分
打开预处理后生成的hello.i文件,我们可以观察到该文件共有3060行代码,较源程序内容大大增多,而源程序中的main函数出现在文件的最后部分,没有发生变化。这是因为在预处理的过程中预处理器对源程序的头文件#include <stdio.h>、#include <unistd.h> 、#include <stdlib.h>进行了展开,文件加入了许多宏定义,并且将其放在了文件前面。
2.4 本章小结
本章介绍了预处理的概念和作用,进行了在Ubuntu中预处理的操作,并浏览分析了生成的hello.i文件,通过代码分析了预处理的工作机制。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编辑器(ccl)将文本文件.i翻译成文本文件.s,它包含一个汇编语言程序。
作用:把源程序翻译成目标汇编程序,进行词法分析和语法分析,分析过程中发现有语法错误,给出提示信息(无法进行后续动作),是高级语言向机器代码转换中重要的中间过程,生成.s文件。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图3.2.1 Ubuntu下编译的命令
图3.2.2 编译生成的hello.s文件
3.3 Hello的编译结果解析
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.3.1 文件
图3.3.1.1 hello.s文件部分代码
.file 声明源文件
.text 代码段
.section .rodata 只读数据段
.align 声明指令或者数据的存放地址的对齐方式
.string 字符串
.global 全局变量
.type 声明一个标识符是函数类型还是数据类型
3.3.2 数据
一、整型常量
图3.3.2.1 常量的保存
if(argc!=4)语句中,整型常量4的值被保存在.text中,作为指令的一部分。同理,for(i=0;i<8;i++)中的整型常量0、8也被保存在.text中,如下图所示。
图3.3.2.2 常量的保存
二、字符串
图3.3.2.3程序中字符串
printf()、scanf()中的字符串被存储在.rodata节中,如上图所示。hello.c中唯一的数组是main函数的第二个参数,数组中每个元素都是指向字符类型的指针。数组两次被调用将参数传递给printf函数。
图3.3.2.4 两字符串作为printf函数的参数
三、局部变量
局部变量存储在寄存器或栈中。
该程序中,局部变量i储存在-4(%rbp)中。
图3.3.2.5 局部变量
3.3.3 赋值
赋值操作利用mov语句。如果是局部变量不赋初值,则在汇编代码中没有体现,只在使用并赋值时才用寄存器等来存储;如果是全局或静态变量变量不赋初值,存放在.bss段;如果是已初始化的全局变量,存放在.data段。
mov指令根据操作数的字节大小分为:
movb:一个字节
movw:字
movl:双字
movq:四字
例如i = 0赋值,如下图所示。
图3.3.3.1 赋值操作
3.3.4 算术操作
在循环中,程序使用了++操作符:for(i=0;i<8;i++),对应的汇编代码如下图所示。
图3.3.4.1 算数操作
3.3.5 关系操作
- 程序中argc!=4是条件判断语句,汇编代码为cmpl $4,-20(%rbp),比较后设置条件码,根据条件码判断是否跳转。
- 程序中i < 8是循环判断条件,汇编代码为cmpl $7, -4(%rbp),比较后设置条件码,根据条件码判断是否跳转。
详情如下图所示。
图3.3.5.1 argc!=4
图3.3.5.2 i < 8
3.3.6 数组/指针/结构操作
主函数main的参数中含有指针数组char *argv[],int main(int argc,char *argv[])。该数组中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别指向printf()、scanf()中的字符串。
数组地址存储在栈中,可以利用栈指针的偏移量来读取。
图3.3.6.1 栈指针偏移
3.3.7 控制转移
汇编语言中先设置条件码,然后根据条件码来进行控制转移。
- if(argc!=4),判断argc是否等于4,如果等于4,则不执行if语句,等于4则执行if语句,对应的汇编代码如下:
图3.3.7.1 if(argc!=4)
2.for(i=0;i<8;i++),每次判断i是否小于8来判断是否退出循环,对应的汇编代码如下:
图3.3.7.2 for(i=0;i<8;i++)
图3.3.7.3 for(i=0;i<8;i++)
3.3.8 函数操作
调用函数时,有以下几个过程。首先要将程序计数器(%rip)设为被调用函数代码的起始地址,在返回时需要将%rip设为调用函数中调用被调用函数后面那条指令的地址。之后要进行数据传递,调用函数向被调用函数提供参数,被调用函数返回相应的返回值给调用函数,同时还要分配和释放相应内存。
该程序中的函数操作:
函数 | 参数 | 返回值 |
main() | int argc,char *argv[] | 0 |
printf() | argv[1],argv[2] | / |
exit() | 1 | / |
sleep() | atoi(argv[3]) | / |
getchar() | 无 | / |
atoi() | argv[3] | int值 |
3.4 本章小结
本章介绍了编译的概念和作用,进行了在Ubuntu中编译的操作,并细致地从几个方面分析了汇编代码以及其与源程序代码之间的联系,借此了解了编译器编译的机制。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将.s汇编程序翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中。
作用:将汇编代码转换为机器指令,并将其打包生成.o文件,为后续的程序的链接和运行做准备。
4.2 在Ubuntu下汇编的命令
命令:gcc -c -o hello.o hello.s
图4.2.1 Ubuntu下汇编的命令
图4.2.2 汇编生成的hello.o文件
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
Linux下生成hello.o文件的elf格式命令:readelf -a hello.o > hello.elf
- ELF头
图4.3.1 ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
2.节头
图4.3.2 节头
描述.o文件中出现的各节名称、类型、地址、偏移量、大小等信息。
3.重定位节
当汇编器生成一个目标模块时,它并不知道数据和代码在内存中具体位置,也不知道该模块引用的外部函数和全局变量的位置。所以当汇编器遇到最终位置未知的目标引用时,就会生成一个重定位条目,告诉链接器在合并目标文件时如何修改引用。代码的重定位条目放在.rel.text中。已初始化数据的重定位条目放在.rel.data中。
图4.3.3 重定位节
.rela.text中保存的是.text节中需要通过重定位被修改的信息:任何调用外部函数或者引用全局变量的指令,调用外部函数的指令,引用全局变量的指令。不需要重定位的信息:调用局部函数的指令,在可执行目标文件中不存在。hello.o中的.rodata中的模式串、puts、exit、printf、slepsecs、sleep,getchar等符号。
.rela.eh_frame节是.eh_frame节重定位信息。
4.符号表
图4.3.4 符号表
.symtab存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
命令:objdump -d -r hello.o > hello1.txt
图4.4.1 反汇编命令
图4.4.2 hello.o的反汇编
与hello.s进行对比分析:
(1)数值表示 :hello.s中的操作数是以十进制表示的,而hello.o的反汇编代码中的操作数是以十六进制表示的,因为16进制与2进制之间的转换比十进制更加方便。
图4.4.3 hello.s数值表示
图4.4.4 hello1.txt数值表示
(2)控制转移:hello.s中使用.L2等段名称进行跳转,而hello.o的反汇编代码使用目标代码的虚拟地址,即主函数+段内偏移量。
以上是关于程序人生-Hello’s P2P的主要内容,如果未能解决你的问题,请参考以下文章