软件工程实验三
Linux下C语言程序的机器级表示
一、实验目的
- 在国产Linux操作系统中进一步掌握 Linux 常用命令
二、实验设备及软件
PC机,国产Linux操作系统
三、 实验内容
add.c源程序内容:
#include "stdio.h"
void main()
{
int x=123,y=-256,sum;
sum=x+y;
printf("sum= %d\n",sum);
}
四、实验过程及分析(每个步骤都要有截图)
1.1)在 home 目录下创建目录 lab3(命令: mkdir lab3);
2)进入目录 lab3(cd lab3);
3)建立一个 C 程序(命令: vim add.c);
进入编辑界面
键入i表示insert进行源文件的编辑:
4)使用带-m32–g -no-pie 参数的 gcc 进行编译。-m32,使用 `-m32` 强制生成32位代码;-g 产生可以供 gdb 调试用的可执行文件 add,带-no-pie ,可以保证编译时会被加载到固定地址。(命令:gcc-m32–g -no-pie add.c –o add);
5)运行程序,查看程序运行的结果是否正确。(命 令: ./add)
(在Linux系统中,ASLR(地址空间布局随机化)是一种安全特性,它通过在每次程序启动时随机化其加载地址来增加攻击者预测程序内存布局的难度。因此,在没有ASLR的情况下,只读代码区(.text段)可能被加载到0x08048000这样的地址,但是ASLR会使得这个地址在每次程序启动时都不同。为了要使程序不受ASLR影响,被加载到一个固定的地址,如0x08048000,你需要禁用ASLR。以下是其中一种方法:gcc -no-pieadd.c -oadd)
2.1)对可执行目标文件 add 进行反汇编,并把反汇编后的内容保存到 add.txt 的文本文件中(命令:objdump -S add >add.txt )
objdump 命令中使用 -S (注意是大写的S)选项参数,会在反汇编后的机器级代码中会添加C源代码,以便理解 C语言源程序与 IA-32 机器级指令之间的对应关系。
2)使用 cat 或 less 命令查看main 函数对应的机器代码文件(命令:less add.txt),内容较多时,继续按 enter,直到显示完所有内容,退出C源文件时,用q或quit命令。
3)反汇编后的 C语句和对应的机器级指令如下:( 汇编后的虚拟地址可能因机器不同而不同)
3 .尝试从汇编指令分析两个数加法的 C程序的执行过程。执行过程中下列步骤对应的汇编指令,在反汇编main函数中的对应情况在图中标出。
1)main函数的栈帧准备;
0x8049162 lea 0x4(%esp),%ecx ; 保存返回地址指针到 ECX 0x8049166 and $0xfffffff0,%esp ; 16 字节对齐栈顶 0x8049169 pushl –0x4(%ecx) ; 把原返回地址重新压栈 0x804916c push %ebp ; 保存旧帧指针 0x804916d mov %esp,%ebp ; 建立新帧指针 0x804916f push %ecx ; 备份 ECX(含原返回地址) 0x8049170 sub $0x14,%esp ; 为 3 个局部变量留出 20 B 空间
此时栈布局(高→低)[预留 20 B]%esp
其中 –0xc(%ebp) 对应 x;–0x10(%ebp) 对应 y;–0x14(%ebp) 对应 sum
2)有初值局部变量赋初值;
0x8049173 movl $0x7b,–0xc(%ebp) ;表示赋值x = 123
0x804917a movl $0xffffff00,–0x10(%ebp) ; 表示赋值y = –256
3)加数和被加数存入相应寄存器;
0x8049181 mov –0xc(%ebp),%edx ; 将x(123)存储在edx中
0x8049184 mov –0x10(%ebp),%eax ; 将y(-256)存储在eax中
4)进行加法运算,并存结果到寄存器;
0x8049187 add %edx,%eax表示eax = eax + edx所以eax等于负256加123等于负133
5)将和的结果存入sum对应的存储单元;
0x8049189 mov %eax,–0x14(%ebp)把sum的结果存入对应存储单元,sum=-133
6)打印sum的内容。
0x804918c sub $0x8,%esp表示再次 16 字节对齐;0x804918f pushl –0x14(%ebp) 表示第2实参sum值;0x8049192 push $0x804a008则表示第 1 实参:格式串 "sum = %d\n";0x8049197 call 8049030 printf@plt表示调用库函数;最后0x804919c add $0x10,%esp 最后表示清栈
3.使用调试工具 gdb(命令: gdb add),提示符变为(gdb)
5. 使用 list 和 run 命令查看源文件和运行程序
6.单步运行程序(此处涉及到指令、寄存器、数值、地址的信息,均为举例,需要根据实验的实际情况填写)
1)重新在add程序的 开始main处设置断点(命令: break main);
2)然后运行程序(命令:run),程序停在第(三)行,此时变量 x 完成赋值与否?(否)
3)查看eip的内容(命令:i r eip),显示 eip 内容是(0x8049173 )(不同时刻或不同机器可能不一样),eip是IA-32 的指令指针寄存器,存储的内容是下一条要执行的指令的地址,根据当前eip的内容判断,下一条要执行的指令是(movl $0x7b, -0xc(%ebp))(填写C语句)。
显示的 eip 内容是 0x8049173。eip 是 IA-32 架构中的指令指针寄存器,它指向下一条将要执行的指令的地址;根据之前的机器码代码,可知地址 0x8049173 对应的指令是 movl $0x7b, -0xc(%ebp),这条指令的作用是将立即数 0x7b(即十进制的 123)移动到栈帧中 ebp-0xc 的位置,也就是局部变量 x 的位置。
4)查看变量 x
int x = 123,对应的汇编指令为:movl $0x7b,-0xc(%ebp),运行时指令会将 x 的值123存储到内存地址为:R[ebp]-c的单元中。
查看ebp的值(命令 i r ebp ),ebp的内容为(0xffffbb68)(ebp的内容可能会因为不同次运行而不同),所以变量x的实际存储地址为:0xffffbb68-0xc=0xffffbb5c。
查看地址0xffffbb5c的值(命令 x/1xw0xffffbb5c),按4 个字节为一个信息组的值,(也可以用命令 x/4xb查看按字节显示 )。
可以看出当前该地址存储的数据为0x080491cb,因为语句 int x=123 还没有被执行,所以当前的值为一个随机数。 (说明什么?又应该注意什么?)
程序执行到断点位置时,变量 x 还没有被赋值,局部变量 x 的赋值语句 int x = 123; 会将值 123 存储到 x 的内存地址中。但是,由于程序还没有执行到这一行,所以 x 的内存地址中仍然包含的是之前的值,这个值可能是程序之前运行时留下的,也可能是内存初始化时的值,因此为随机数
使用命令 si,单步运行一条机器指令,看此时 eip 的值(命令: i r eip )。
此时再查看 x 的存储地址0xffffbb5c的值(命令 x/1xwb或x/4xb0xffffbb5c),此时0xffffbb5c存储的值为0x0000007b,说明已经执行 x=123 的赋值。即存储地址为0xffffbb5c的存储单元中已存入123的值。
5)查看 y 的赋值情况
int y = -256 对应的汇编指令为:movl $0xffffff00,-0x10(%ebp),可以看出y的存储地址为:R[ebp]-0x10。
此时基址指针寄存器 ebp的值本身为0xffffbb68,得出y 的具体存储地址为:0xffffbb68 - 0x10 =0xffffbb58。
y被赋值前,查看地址0xffffbb58的值(命令 x/1xw0xffffbb58),可以看出当前该地址存储的数据为0xffffbc1c,值为一个随机数。
再单步运行一条机器指令(命令: si ), 此步完成 y 的赋值。
再使用命令 i r eip 查看 eip 的值。
y被赋值后,再查看 y 的存储地址0xffffbb58的值(命令 x/1xwb或x/4xb0xffffbb58)
使用命令 x/4xb 按字节显示或者使用命令 x/1xw 查看 y 的存储空间0xffffbb58的值,
此时0xffffbb58存储的值为0xffffff00(-256的机器码),说明已经执行y=-256 的赋值。即存储地址为0xffffbb58的存储单元中已存入-256的值。
6)继续2次单步运行(命令:si),然后查看寄存器 ecx 和 edx 的值
R(ecx)= 123, R(edx)=-256,x 和 y 的值已经送入寄存器ecx和 edx 中。
7) 继续1次单步运行(命令:si) 然后查看寄存器 ecx 和 edx 的值
R(ecx)= 123, R(edx)=-133 , x加y的和123+(-256)=-133,已经存入寄存器 edx中
8)继续1次单步运行(命令:si) 然后查看存储器的值
由汇编指令(mov %edx,-0x14(%ebp))可知, x+y 的和sum存入了地址为 0xffffbb68 - 0x14 = 0xffffbb54的存储单元。
M(0xffffbb54) = 0xffffff7b =(-133)10,说明将变量 x 和 y 相加的和已经送入相应存储器的存储单元中, 地址为 0xffffbb54,这里忽略程序运行的下一步“打印”。
9)用 print&命令查看变量 x 、y 和 sum的存储地址,与上面通过 EBP 值计算得到的 x 、y和 sum 的地址对比,结果一致,说明上面的计算是正确的。
10)再进行 2次单步运行(命令:n(是 next的简称)),直至程序运行结束。(next单步运行,不进入函数内部,si是执行1条汇编指令,会进入函数内部)
再次查询变量 x、y 和 sum 的存储地址,发现已经不存在, 说明局部变量 x、y 和 sum 只在程序运行中(进程中)暂时生存,程序结束,变量释放。请思考,此时代码区的内容释放没有?
11)退出 gdb(命令:quit或者简写 q)(根据需要:可以随时退出, 或再重新进入 gdb,等同于重新开始实验)
12)根据自己实验结果,参考下表画出可执行文件 add的虚拟存储空间(存储器映像)
(高地址在上, 低地址为下),其中用户栈为临时生存,程序结束后用户栈消失。(需要把下表中绿色部分内容替换为自己实验的数据)
基于IA-32的Linux用户进程存虚拟空间的分配情况(即存储器映像)
<span style="font-family:宋体; <p>mso-ascii-font-family:宋体;<br>mso-fareast-font-family:宋体;<br>mso-bidi-font-family:宋体;<br>font-variant:normal;<br>text-transform:none;<br>mso-fareast-language:ZH-CN;<br>mso-font-kerning:1.0pt"><o:p><span leaf=" "=""> | <o:p> | <o:p> | <o:p> | 虚拟地址 | 代码或数据 |
<o:p> <o:p> <o:p> 用户栈 | <o:p> | <o:p> | EBP | 0xffffbb68 | EBP 栈基址指针 |
<o:p> | <o:p> | <o:p> | …… | <o:p> |
<o:p> | x | EBP-0xc | 0xffffbb5c | 0x0000007b(十进制 123,变量 x 的值) |
<o:p> | y | EBP-0x10 | 0xffffbb58 | 0xffffff00(十进制 - 256,变量 y 的值) |
<o:p> | sum | EBP-0x14 | 0xffffbb54 | 0xffffff7b(十进制 - 133,变量 sum 的值) |
<o:p> | <o:p> | <o:p> | <o:p> | …… | <o:p> |
<o:p> 只读代码区: | <o:p> | Sum=x+y | 加法指令地址 | 0x8049187 | 01 d0(add %edx, %eax)、8b 55 f4(mov -0xc(%ebp),%edx)、8b 45 f0(mov -0x10(%ebp),%eax) |
<o:p> | y=-256 | 赋值指令地址 | 0x804917a | C7 45 f0 00 ff ff ffmovl $0xffffff00,-0x10(%ebp)) |
<o:p> | x=123 | 赋值指令地址 | 0x8049173 | C7 45 f4 7b 00 00 00(movl $0x7b,-0xc(%ebp)) |
<o:p> | <o:p> | <o:p> | ……. | <o:p> |
<o:p> | main | 函数起始地址 | 0x8049162 | 8d 4c 24 04(lea 0x4 (% esp),% ecx)、83 e4 f0(and $0xfffffff0,% esp |
<o:p> | <o:p> | <o:p> | <o:p> | ……. | <o:p> |
五、实验总结与感受
实验总结
本次实验内容主要是 Linux 环境下 C 语言程序的机器级表示:创立目录之后,先编写源程序,再通过gcc -m32 -g -no-pie add.c -o add指令生成32位的机器码;再用objdump -S add > add.txt指令将机器指令对应的16进制形式的虚拟存储地址、机器指令的16进制表示、可执行文件反汇编后的汇编指令输出,直观建立了高级语言与机器指令的对应关系;
然后通过分析反汇编代码,我学习了了 C 语句对应的机器指令逻辑:例如int x=123对应movl $0x7b,-0xc(%ebp)(将 16 进制 0x7b 即十进制 123 存入 EBP-0xc 的栈地址),sum=x+y对应 “mov -0xc(%ebp),%edx(读取 x 到 EDX)→mov -0x10(%ebp),%eax(读取 y 到 EAX)→add %edx,%eax(计算和存入 EAX)→mov %eax,-0x14(%ebp)(将结果写入 sum 的栈地址)等等,进一步加深了变量存储在栈帧,运算则是依靠寄存器的原理,同时使用gdb进行调试,通过设置断点和单步执行,同时用i r eip/ebp等指令查看内存值,追踪了栈帧的生命周期:局部变量 x、y、sum 分别对应 EBP-0xc、EBP-0x10、EBP-0x14 的地址并在函数结束后地址失效,最后根据实验数据梳理了虚拟内存的映像,验证了虚拟内存地址分配的规律
实验感受
本次实验我通过实践(反汇编和gdb一步步的调试),让我在操作中对“内存具体是哪块区域”“函数调用如何操作栈”“加法运算在寄存器中如何执行”等等问题进行了回答,概念也逐渐变得清晰
我同时对C语言这一高级编程语言的背后原理有了更加丰富的理解,对计算机系统有了更加底层的认识,同时明白课上的知识不能死记硬背,要通过操作才能内化于心
附录 :IA-32 的 Linux 用户进程的虚拟内存空间的分配(存储器映像)
对于 IA-32 + Linux 系统, i386 System V ABI 规范规定可执行目标文件与虚拟地址空间的存储器映像:
- 0xffffffff~0xc0000000 共 1G:用于存放内核程序;
- 0xc0000000 开始向下:用户栈,用于存放局部变量,函数调用形参等;
- 0x40000000 开始向上:共享库, 用于存放 printf 等库函数;
- 0x40000000 开始向下:用户堆, 用于 malloc 等函数分配的内存;
- 0x08048000开始向上:只读代码区, 用于加载存放用户程序和代码、全局变量等可读写数据;
- 0x00000000~0x08047fff:未使用。