gdb反汇编详解C函数底层实现笔记(程序堆栈、内存分配)

简介: 以下是在读《深入理解计算机系统》前面的章节“程序的机器级表示”时,自己动手在linux上使用了gdb对一个简单的C程序进行反汇编,通过不懈的努力终于查清楚弄明白了绝大多数的语句。
以下是在读《深入理解计算机系统》前面的章节“程序的机器级表示”时,自己动手在linux上使用了gdb对一个简单的C程序进行反汇编,通过不懈的努力终于查清楚弄明白了绝大多数的语句。且均以注释的形式列在汇编语句后面。

      所有这些注释大概花了整整一天时间,不过还好,感觉对于C程序的机器级实现终于算是有了一个比较透彻的理解,对于以前编译出现的有些bug的原因有了一种原来如此的感慨。感觉这段代码及注释对自己或者大家都可能有用,所以稍作整理放于此:

 

说明:
反汇编指令使用的是:gdb disas [function-name]。另外也可以使用:objdump -d/-D obj-name对整个程序进行反汇编(通过这个反汇编,你可以发现main函数并非一个程序的入口而是__start函数!)。

 

程序很简单,就两个函数:sum和main,源码如下:
#include<stdio.h>

int sum(int x, int y)
{
 int accum = 0; 
 int t;
 t = x + y;
 accum += t;
 return accum;
}

int main( int argc, char **argv)
{
   int x = 1, y = 2;
   int result = sum( x, y );
   printf("\n\n     result = %d \n\n", result);
   return 0;
}

编译源程序,不使用优化选项并反汇编分析:

gcc -o test test.c得到test
gdb test
(gdb) disas sum
(gdb) disas main
查看反汇编后的代码
Dump of assembler code for function sum:
0x08048354 <sum+0>:     push   %ebp  //esp <- esp-4
0x08048355 <sum+1>:     mov    %esp,%ebp
0x08048357 <sum+3>:     sub    $0x10,%esp
0x0804835a <sum+6>:     movl   $0x0,0xfffffff8(%ebp)   //accum = 0
0x08048361 <sum+13>:    mov    0xc(%ebp),%eax    //gotcha! ^_^
0x08048364 <sum+16>:    add    0x8(%ebp),%eax    // x + y
0x08048367 <sum+19>:    mov    %eax,0xfffffffc(%ebp)  // t = x + y
0x0804836a <sum+22>:    mov    0xfffffffc(%ebp),%eax 
0x0804836d <sum+25>:    add    %eax,0xfffffff8(%ebp)  // accum = accum + t
0x08048370 <sum+28>:    mov    0xfffffff8(%ebp),%eax // 返回值是放在eax中的
0x08048373 <sum+31>:    leave  //恢复父函数的堆栈框指针:esp<-ebp, pop ebp,在地址上的表现就是堆栈顶底均回到之前的地方!
0x08048374 <sum+32>:    ret     //函数返回,回到上级调用:pop eip?

main函数反汇编结果:
0x08048375 <main+0>:    lea    0x4(%esp),%ecx
0x08048379 <main+4>:    and    $0xfffffff0,%esp //使栈地址16字节对齐!这里也说明了栈向下(低地址)生长的优点了:地址对齐操作总是是esp指向当前位置的下方!(lihux自己悟出的^_^)
0x0804837c <main+7>:    pushl  0xfffffffc(%ecx)
0x0804837f <main+10>:   push   %ebp   //保存main之前函数的栈基址
//pushl push都使用了的原因:当要压栈的对象已经确定(也就是说已经知道是字节、字或者双字),那么使用push就不会产生歧义,也就是说汇编器可以自己判断自己要操作的是什么长度的操作对象;但是当汇编器不能自己判断操作对象长度时,就需要使用pushl之类的指令来指明操作对象长度(类似的还有mov,movl;lea,leal等);
上面的%ebp指的是esp寄存器吧,这是一个32位的寄存器,汇编器是知道这个寄存器存放的是dworld,不需要显式的指明也没有歧义;而 0xfffffffc(%ecx)是使用基址寻址方式指向的一个内存空间,内存是连续的,汇编器不能仅仅根据一个内存地址就判断出那里存放的数据的长度,所以直接这样写就会产生歧义,所以要使用pushl之类的指令来指明要操作的数据的长度;
0x08048380 <main+11>:   mov    %esp,%ebp  //在main之前函数的栈下方建main的栈,基址给ebp
0x08048382 <main+13>:   push   %ecx
0x08048383 <main+14>:   sub    $0x24,%esp  //栈顶指针下移24,以留作main函数用?是的!^_^。关于栈对齐问题:可以看到,在<main+4>执行时完毕之后栈顶指针esp已经16字节对齐,而在执行到当前<main+14>指令前,又进行了三次压栈操作,每次四字节,共12字节,所以这里用0x24,这样,又重新使esp16字节对齐了(同时还必须保证分配的空间不小于main函数需要存放局部变量的空间^_^)。
0x08048386 <main+17>:   movl   $0x1,0xfffffff0(%ebp)  //主函数实参存放于内存的最高处,x = 1
0x0804838d <main+24>:   movl   $0x2,0xfffffff4(%ebp) //y = 2
0x08048394 <main+31>:   mov    0xfffffff4(%ebp),%eax
0x08048397 <main+34>:   mov    %eax,0x4(%esp)           //x的形参存入Mem本地空间,供子函数调用
0x0804839b <main+38>:   mov    0xfffffff0(%ebp),%eax
0x0804839e <main+41>:   mov    %eax,(%esp)           //由此可以看出:调用函数sum之前,需先将形参放到栈顶!
0x080483a1 <main+44>:   call   0x8048354 <sum>  //call调用,先将下一条命令的地址压入堆栈,所以esp<- esp-4
0x080483a6 <main+49>:   mov    %eax,0xfffffff8(%ebp) //result = sum(x,y),返回值在eax中
0x080483a9 <main+52>:   mov    0xfffffff8(%ebp),%eax
0x080483ac <main+55>:   mov    %eax,0x4(%esp)
0x080483b0 <main+59>:   movl   $0x80484a0,(%esp)  //字符串地址
0x080483b7 <main+66>:   call   0x8048298 <>
0x080483bc <main+71>:   mov    $0x0,%eax
0x080483c1 <main+76>:   add    $0x24,%esp   //?何为
0x080483c4 <main+79>:   pop    %ecx
0x080483c5 <main+80>:   pop    %ebp
0x080483c6 <main+81>:   lea    0xfffffffc(%ecx),%esp
0x080483c9 <main+84>:   ret   
0x080483ca <main+85>:   nop   
0x080483cb <main+86>:   nop   
0x080483cc <main+87>:   nop   
0x080483cd <main+88>:   nop   
0x080483ce <main+89>:   nop   
0x080483cf <main+90>:   nop  

 

编译源程序,使用 -O1 优化选项并反汇编分析:

使用 -O1优化后的程序反汇编后的结果:可见优化主要在减少了存取内存的次数,节省了内存空间
Dump of assembler code for function sum:
0x08048354 <sum+0>:     push   %ebp
0x08048355 <sum+1>:     mov    %esp,%ebp
0x08048357 <sum+3>:     mov    0xc(%ebp),%eax
0x0804835a <sum+6>:     add    0x8(%ebp),%eax
0x0804835d <sum+9>:     pop    %ebp
0x0804835e <sum+10>:    ret   
End of assembler dump.

Dump of assembler code for function main:
0x0804835f <main+0>:    lea    0x4(%esp),%ecx
0x08048363 <main+4>:    and    $0xfffffff0,%esp
0x08048366 <main+7>:    pushl  0xfffffffc(%ecx)
0x08048369 <main+10>:   push   %ebp
0x0804836a <main+11>:   mov    %esp,%ebp
0x0804836c <main+13>:   push   %ecx
0x0804836d <main+14>:   sub    $0x14,%esp   //可见优化后的程序自身预留空间也减少了。
0x08048370 <main+17>:   movl   $0x2,0x4(%esp)
0x08048378 <main+25>:   movl   $0x1,(%esp)
0x0804837f <main+32>:   call   0x8048354 <sum>
0x08048384 <main+37>:   mov    %eax,0x4(%esp)
0x08048388 <main+41>:   movl   $0x8048480,(%esp)
0x0804838f <main+48>:   call   0x8048298 <>
0x08048394 <main+53>:   mov    $0x0,%eax
0x08048399 <main+58>:   add    $0x14,%esp
0x0804839c <main+61>:   pop    %ecx
0x0804839d <main+62>:   pop    %ebp
0x0804839e <main+63>:   lea    0xfffffffc(%ecx),%esp
0x080483a1 <main+66>:   ret   
0x080483a2 <main+67>:   nop   
0x080483a3 <main+68>:   nop   
0x080483a4 <main+69>:   nop

 

编译源程序,使用优化选项 -O2并反汇编分析:

使用 -O2优化后的程序反汇编后的结果:可见较-O1级优化又有大幅的缩短(代码长度)
Dump of assembler code for function sum:
   0x08048410 <+0>: push   %ebp
   0x08048411 <+1>: mov    %esp,%ebp
   0x08048413 <+3>: mov    0xc(%ebp),%eax
   0x08048416 <+6>: add    0x8(%ebp),%eax
   0x08048419 <+9>: pop    %ebp
   0x0804841a <+10>: ret   
End of assembler dump.

Dump of assembler code for function main:
   0x08048420 <+0>: push   %ebp
   0x08048421 <+1>: mov    %esp,%ebp
   0x08048423 <+3>: and    $0xfffffff0,%esp
   0x08048426 <+6>: sub    $0x10,%esp
   0x08048429 <+9>: movl   $0x3,0x8(%esp)   //我晕:这优化的也太厉害了吧:直接计算出了1+2=3!
   0x08048431 <+17>: movl   $0x8048510,0x4(%esp)
   0x08048439 <+25>: movl   $0x1,(%esp)
   0x08048440 <+32>: call   0x804832c <>
   0x08048445 <+37>: xor    %eax,%eax   //因为IA32中xor 比 mov 速度快!
   0x08048447 <+39>: leave 
   0x08048448 <+40>: ret   

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
40 3
|
1月前
|
存储 前端开发 Java
Kotlin教程笔记 - MVVM架构怎样避免内存泄漏
Kotlin教程笔记 - MVVM架构怎样避免内存泄漏
26 2
|
2月前
|
NoSQL 测试技术
内存程序崩溃
【10月更文挑战第13天】
145 62
|
20天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
49 6
|
22天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
52 1
|
1月前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
2月前
|
存储 程序员 编译器
简述 C、C++程序编译的内存分配情况
在C和C++程序编译过程中,内存被划分为几个区域进行分配:代码区存储常量和执行指令;全局/静态变量区存放全局变量及静态变量;栈区管理函数参数、局部变量等;堆区则用于动态分配内存,由程序员控制释放,共同支撑着程序运行时的数据存储与处理需求。
153 21
|
2月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
存储 程序员 编译器
C语言——动态内存管理与内存操作函数
C语言——动态内存管理与内存操作函数