本文电子版(Droplr)

一个windows下的小工具,查看二进制代码对应的汇编码(Droplr)
本文主要对main函数编译后生成的汇编码进行观察,为了简单起见,main函数的内容为空。实验方法如下:首先在不同环境下编译源代码,收集生成的可执行文件;随后将可执行文件使用IDA Pro(版本为5.5,这里赞一下强大的IDA!)进行反汇编;最后观察main函数的汇编码(所有汇编码格式都是Intel风格的),进行分析与比较。本文重点在于讨论一些最基本的概念,有助于读者熟悉各种环境生成的汇编码,更好地进行二进制分析。需要注意的是,在C语言的层面来看,main函数是程序的起始入口,但实际上对于可执行文件来说,CPU真正执行的第一条指令往往并不是main函数汇编码的第一条指令,这里仅分析main函数的汇编码,对于可执行文件中的其他部分就忽略不谈了。

源代码

int main()

{

return 0;

}

VC环境

winxpsp3+vc2010_release

首先以vc2010(Microsoft Visual C++ 2010 Express)release模式生成的可执行文件为例,上图为main函数的汇编码,可以看到,内容十分简单。

第一条指令xor eax,eax是对eax进行异或运算,这是对寄存器赋0值的一种常见形式,通常约定把函数的返回值放在eax中返回(32位,16位放在ax中),因此可见这是在为return 0;语句做准备;第二条指令retn是过程近(near)返回指令,从堆栈弹出返回地址压到eip中,与之对应的还有远(far)返回指令retf,首先弹出eip,然后弹出cs(其实,对于现代操作系统来讲,每个进程都有其单独的相同的逻辑地址空间,段寄存器的值由操作系统设定且固定,与之相关的汇编指令也就很少再使用了),而指令ret根据PROC伪指令,自动判断是近返回还是远返回(当然,从可执行文件是看不到伪指令的)。

仅看main函数的汇编指令,可以说和C语言的源代码是一样的简单。

接着观察vc2010debug模式生成的可执行文件的汇编码,见上图,可以看到相比release模式要复杂许多,之所以会有这样的区别,是因为在debug模式包含有调试信息,且未进行优化,release模式把一些执行过程优化掉了。

下面简单解释一下代码的含义:

因为main函数也是函数,所以它与函数的执行过程相同:调用前传递函数参数(本例中没有参数),进入时为函数的局部变量分配空间,并在退出时释放这些空间。这里要介绍一下栈帧(stack frame)的概念,栈帧,也称为活动记录(activation record),它是为传递的参数、子例程的返回地址、局部变量和保存的寄存器保留的堆栈空间。栈帧的两端是以两个指针定界的,寄存器ebp作为帧指针,表示栈帧的底部,等于函数调用前运行栈的栈顶指针的值,在函数调用过程中不改变其值,当函数调用结束时可以通过帧指针的值将栈帧空间释放掉;寄存器esp是运行栈的栈顶指针,同时也表示栈帧的顶部,在运行时是可以改变的(见下图)。

第一条汇编码push ebp首先保存ebp的值,因为马上将使用它作为帧指针;第二条汇编码mov ebp,esp将ebp赋为当前的栈顶指针,也就是帧指针,从这时开始,ebp就被作为寻址所有子例程参数的基址指针使用了;第三条汇编码sub esp,0C0h是将栈(也是栈帧)扩大0C0h大小,但此时并没有在其中填充内容,这样做通常是为了给局部变量留出空间,这里明明没有任何局部变量,那0C0h大小是如何跑出来的呢,稍后将解释这个问题。

根据惯例,eax、edx、ecx的值由调用方负责保存,即在函数内部这3个寄存器可以随便使用;而ebx、esi、edi的值由被调用方负责保存,使用之前必须将原先的值保存到栈中,这也是为什么接下来的3条代码将这3个寄存器分别压入栈中的原因。

接下来的几条指令是专门用作调试。lea edi,[ebp+var_C0]实际上就是把地址存入到edi中,地址的值就是刚才留出0C0h大小的区域的最低位置;接着对ecx赋值为30h;对eax赋值为0CCCCCCCCh;最后执行指令rep stosd,这条指令的含义是将stosd这条指令重复ecx(即30h)次,而stosd指令的含义是将eax的值(0CCCCCCCCh)复制到内存中,内存的地址为es:edi,每次执行后edi改变,这条指令合在一起的意思就是将es:edi为起始地址,大小为ecx*4的内存的所有字节均设为0CCh,就是把刚才留出的0C0h(30h * 4 = 0C0h)的空间全部填为0CCh。

只所以这样做是为了便于调试:0CCh是汇编指令int 3的二进制码,这条指令的意思是调用3号中断服务程序,会产生一个断点,如果想感受一下实际的运行效果可以用下面的代码:

int main()

{

__asmint 3;

return 0;

}

而将一大片区域都设置成为断点的意义在于:若程序存在漏洞,执行时可能会误执行这片区域中的内容,因为这片区域内容都是0CCh,运行时立刻报错,便于发现漏洞,说白了就是在栈中有用的数据旁边附着陷阱,一个正确的程序执行是绝不会踏入陷阱中的。

这个过程结束之后,就是之前介绍过的xor eax,eax,如果这个main函数有其他语句,那汇编出来的代码就会在rep stosd与xoe eax,eax之间。接着还原edi、esi、ebx寄存器的值。

此时函数的执行已经基本结束,之前开辟出的栈帧的使命已经结束,mov esp,ebp将esp恢复到函数调用前的状态,接着恢复ebp,最后返回,整个过程结束。

此外,由于栈帧的创建与释放十分普遍,intel提供了两条简化的汇编指令enter和leave。其中,enter imm,0与push ebp; mov ebp,esp; sub esp,imm相等价;leave与mov esp,ebp; pop ebp相等价。

winxpsp3+vc2008_debug

情况与vc2010下完全相同,没有看出编译器的变化。

win7+vc2010_release

情况与winxp下的相同,但虽然找到了main函数,却不知为何没有对其进行命名。虽然vc的版本是VS2010pro,但按说编译器应该是相同的。

win7+vc2010_debug

与release模式相同,仅是main名称没有识别出来的问题。

GCC模式

以下实验使用的编译命令均为gcc -o test test.c

Ubuntu10.04+linux2.6.32+gcc4.4.3

汇编码见上图,结合上面讲述的栈帧概念很好理解。如果仔细留意的话,会发现与vcdebug的汇编码相比,这里没有对edi、esi、ebx的保存与回复操作,而且因为没有用到esp,所以最后也没有mov esp,ebp的操作。

我又设优先级为O0(gcc默认的优先级是O1)重新编译了一遍,发现结果是相同的,看来gcc编译时会记录寄存器的使用情况。

我又以优先级O2、O3重新编译,结果如下图(两个结果相同):发现首先mov eax,0变成xor eax,eax,异或运算执行速度要快于传送运算;接着发现这条优化后的指令位置向上移动,跑到了mov ebp,esp指令的上面,这个原因我就不清楚了。

Ubuntu9.04+linux2.6.28+gcc4.3.3

再来看一个较低版本的,先是O0(O1相同)的(见左下图):

可以看到比上一个版本情况复杂了不少,而且还有错误信息提示(见最后一行),说是栈指针(stack pointer)分析失败(sp-analysis failed),造成这个错误的原因是因为堆栈不平衡而导致的,在IDA中点击Options->General->Disassembly,将选项stack pointer打勾,汇编码就会显示栈指针的情况,如右上图,最后一条指令retn前为“004”而不是“000”,因此会报错,接下来我们分析一下这段汇编码的含义:

汇编指令

解释

lea ecx, [esp+arg_0]

将esp加4的得到的地址存入ecx,即ecx指向第1个参数

and esp, 0FFFFFFF0h

这里调整esp的值,使之末4位0(即按16字节对齐)

push dword ptr [ecx-4]

将最开始esp所指的地址的值(即函数返回地址)入栈

push ebpmov ebp, esp

建立栈帧

push ecx

压入ecx(即old esp+4)保存,这条指令之后进入main函数

mov eax,0

由于我们的main函数没有内容,所以直接开始返回操作

pop ecxpop ebp

恢复ecx(即old esp+4)与ebp

lea esp, [ecx-4]retn

恢复esp然后返回

看到这里,你会发现esp在执行前后是相同的,其实当main函数中出现了跳转指令后,错误提示就消失了,也许这只是IDA的一个bug吧。见左下图:

可以看到,同一条指令lea esp, [ecx-4]在不同的函数中其栈指针的变化居然不同,因此大家不必为这个错误提示而在意。

这段代码多出来一个push操作:push dword ptr [ecx-4]。它是做什么用的,我只能做如下解释:

首先先了解一下call指令的操作(前面已经提到过,即使是main函数,实际也是被另一个函数使用call调用),call指令将下一条指令的地址(即eip寄存器中的值,就是函数返回地址)压入栈中(即push eip),然后将控制转移到目的地址(即eip=目的地址)。

回忆栈帧的示意图可以得知,栈中函数返回地址标志着调用帧的结束,而创建栈帧第一步压入栈的ebp标志着被调用帧的开始,它们之间形成一条分界线。如右上图黑色的粗线:

而现在在push ebp之前要进行16字节的对齐操作,栈中压入的eip与ebp就可能存在一个空隙,也许是为了保证栈帧格式的完整性,在对齐操作之后,push ebp之前,重新压入一次返回地址,因此增加了一条push dword ptr [ecx-4],ecx-4指向的就是返回地址。注意,我说的只是也许,因为我几次测试也没有发现为什么要压入这个返回地址,只能做这样的猜测。

Mac OS X 10.6.4+x64+gcc4.2.1

最后分析mac下的情况,如左上图所示(优先级为O0,O1),出现了没有见过的rbp,rsp;其实这是64位的寄存器,它们的作用分别与32位的寄存器ebp,esp相对应;其实原先的通用e系列通用寄存器都有与之对应的r系列寄存器,此外intel64位模式还新增了8个通用寄存器(r8-r15),以后有机会我会测试mac下的gcc有没有针对64位作出专门的优化,使用这些新增的寄存器。

右上图为优先级O2、O3的结果,用异或操作取代了传送操作,很简单。

总结

这篇文章展示了一些常见环境下main函数的汇编码,并简单的进行分析,内容比较粗浅。其实只要能完全理解栈帧的概念,不管将来遇到什么样的函数汇编码,都能轻松突破各种混乱的操作,找到其关键内容,这也正是本文的初衷。今后经过进一步的学习,我还将尝试完整地解析各个操作系统的可执行文件的内容,而不仅仅只是一个空的main函数。

转载于:https://www.cnblogs.com/HeroZearin/articles/2682108.html

main函数的汇编代码相关推荐

  1. 在 main 函数之前执行代码(C 语言)

    main 函数 又称主函数,是程序执行的起点. 在 main 函数之前 那么,我们有办法在 main 函数之前执行代码吗? 答案是肯定的. 先上代码 before_main.c #include &l ...

  2. 问题十一:用条件编译(#if…#endif)避免 main函数中测试代码在测试完成后就删除

    #define testNumber 3 /* 1: output the first image 2: test "int &ri,int& ri,int*&pri ...

  3. [汇编与C语言关系]2. main函数与启动例程

    为什么汇编程序的入口是_start,而C程序的入口是main函数呢?以下就来解释这个问题 在<x86汇编程序基础(AT&T语法)>一文中我们汇编和链接的步骤是: $ as hell ...

  4. ARM(IMX6U)裸机C语言版本LED驱动实验(汇编进入处理器SVC模式、SP堆内存、跳转main函数、链接起始地址)

    参考:Linux之ARM(IMX6U)裸机C语言LED驱动实验–驱动编写,编译 作者:一只青木呀 发布时间: 2020-08-11 11:20:17 网址:https://blog.csdn.net/ ...

  5. linux 为什么 c语言,为什么C程序里一定要写main函数

    为什么C程序里一定要写main函数 一. 学习过程 编写程序f.c: 对其进行编译,正常通过,再对其进行连接,出现错误: 显示的出错信息为: 翻译成中文是:在c0s模块没有定义符号'_main'. 那 ...

  6. 为什么c程序里一定要写main函数

    一. 学习过程 编写程序f.c: 对其进行编译,正常通过,再对其进行连接,出现错误: 显示的出错信息为: 翻译成中文是:在c0s模块没有定义符号'_main'. 那么这个错误信息可能与文件c0s.ob ...

  7. C++程序设计基础(8)main函数

    注:读<程序员面试笔记>笔记总结 1.知识点 (2)main函数的形式 1 //first type 2 int main() 3 //second type 4 int main(int ...

  8. C语言程序可以没有main函数

    原文链接:http://www.wufangbo.com/175871/ 学习C语言的同学都知道,每个C程序要有一个main函数,程序从main函数开始执行,在main函数中结束.但事实上,C程序也可 ...

  9. main函数的入口函数

    操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,并且负责调用main函数, 运行这些代码的函数称为入口函数或入口点(Ent ...

最新文章

  1. openssh漏洞_漏洞预警 | OpenSSH 命令注入漏洞
  2. javascript实现yield
  3. python画切片图_python切片操作
  4. 学习笔记68—PhotoShop常见快捷键
  5. 解读Facebook CAN:如何给人工智能赋予艺术创作的力量
  6. JavaScript中带有示例的Math.log()方法
  7. CSS选择器必备的3个知识点
  8. 画质评测|一次关于视频画质的探(zhǎn)讨(xiàn)
  9. Unity V3 初步使用 —— 为我的.NET项目从简单三层架构转到IOC做准备
  10. jquery中的过滤filter not的用法以及可以添加this
  11. 网站php镜像小偷,网站被镜像了怎么办?
  12. 工具 IDA Pro
  13. 车牌字符识别算法原理
  14. Unity CityEngine 根据地图中建筑矢量数据批量建模
  15. onlyoffice5.4.2删除字体和添加字体
  16. 定理在数学中的简写形式_初中数学常用的定理大全
  17. 北京文安科技发展有限公司招聘简介
  18. Appium 自动化用例设计(TESTNG 篇)
  19. 你们知道第一个发Nature的中国人是谁吗?
  20. unity2D动画和图片切割

热门文章

  1. ubuntu安装celery报错 encountered error while generating package metadata
  2. ubuntu下matlab添加中文支持
  3. Python爬虫实战案例:爬取新闻资讯
  4. windows系统之间SMB共享文件夹的访问和操作
  5. python批量替换网站静态文件内的域名
  6. 软考中级软件设计师---知识产权(自用)
  7. Java蘑菇钉云签到管理系统源码
  8. 【附源码】计算机毕业设计SSM汽车出租平台
  9. 搭建文件服务器MinIO,快速上手使用
  10. 38Khz红外遥控发射