main函数的汇编代码

本文主要对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”,因此会报错,接下来我们分析一下这段汇编码的含义:

看到这里,你会发现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的结果,用异或操作取代了传送操作,很简单。

时间: 2024-08-29 03:52:52

main函数的汇编代码的相关文章

在C++工程中main函数之前跑代码的廉价方法(使用全局变量和全局函数)

[cpp] view plain copy // test.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> #include <crtdbg.h> /// 在C++工程中main函数之前跑代码的廉价方法 /// 利用全局变量可以赋可变初值的事实 /// mainCRTStartup() => _cin

Delphi的字符串与16进制的相互转换函数的汇编代码

代码 function StrToHex(Const str: Ansistring): Ansistring;asm push ebx push esi push edi test eax,eax jz @@Exit mov esi,edx //保存edx值,用来产生新字符串的地址 mov edi,eax //保存原字符串 mov edx,[eax-4] //获得字符串长度 test edx,edx //检查长度 je @@Exit {Length(S) = 0} mov ecx,edx //

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

为什么汇编程序的入口是_start,而C程序的入口是main函数呢?以下就来解释这个问题 在<x86汇编程序基础(AT&T语法)>一文中我们汇编和链接的步骤是: $ as hello.s -o hello.o $ ld hello.o -o hello 我们用gcc main.c -o main开编译一个c程序,其实际分为三个步骤:编译.汇编.链接 $ gcc -S main.c 生成汇编代码 $ gcc -c main.s 生成目标文件 $ gcc main.o 生成可执行文件 我们

从开机加电到执行main函数之前的过程

1.启动BIOS,准备实模式下中断向量表和中断服务程序 在按下电源按钮的瞬间,CPU硬件逻辑强制将CS:IP设置为0xFFFF:0x0000,指向内存地址的0xFFFF0位置,此位置属于BIOS的地址范围.关于硬件如何指向BIOS区,这是一个纯硬件动作,在RAM实地址空间中,属于BIOS地址空间部分为空,硬件只要见到CPU发出的地址属于BIOS地址范围,直接从硬件层次将访问重定向到BIOS的ROM区中.这也就是为什么RAM中存在空洞的原因. BIOS程序在内存最开始的位置(0x00000)用1K

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

一. 学习过程 编写程序f.c: 对其进行编译,正常通过,再对其进行连接,出现错误: 显示的出错信息为: 翻译成中文是:在c0s模块没有定义符号’_main’. 那么这个错误信息可能与文件c0s.obj有关.那么是什么原因导致编译出错呢? 既然已经将程序编译成了obj文件,那么用之前我们经常使用的link.exe能否将它连接呢?结果是可以的: 用debug查看f.exe: 程序是从06fb:0到06fb:001c,一共29个字节.但是整个程序的代码有541字节: 执行最后一条ret指令,返回到b

高级语言里的函数在汇编里的实现方式

一. 学习过程 在高级语言中我们为什么要用变量呢?因为我们要存储数据,而且因为要使用循环等语法结构,存储的数据需要不断地变化,变量的特性可以很好地解决这个问题.在前面我已经讨论过了,变量的声明实际上就是在内存中开辟一个内存空间,我们在汇编语言里使用循环,主要是把数据存在si.di等寄存器中来进行操作,存储数据是把数据放在寄存器.内存空间(普通的和栈)里面.编写程序ur1.c,并编译连接: 用debug加载ur1.exe,用u命令查看编译后的机器码和汇编代码: 发现main函数中的代码没有出现,用

java main函数修饰符

public static void main(String[] args){-} 下面分别解释这些关键字的作用: (1)public关键字,这个好理解,声明主函数为public就是告诉其他的类可以访问这个函数. (2)static关键字,告知编译器main函数是一个静态函数.也就是说main函数中的代码是存储在静态存储区的,即当定义了类以后这段代码就已经存在了.如果main()方法没有使用static修饰符,那么编译不会出错,但是如果你试图执行该程序将会报错,提示main()方法不存在.因为包

没有main函数的helloworld

几乎所有程序员的第一堂课都是学习helloworld程序,下面我们先来重温一下经典的C语言helloworl /* hello.c */ #include <stdio.h> int main() { printf("hello world!\n"); return 0; } 这是一个简单得不能再单的程序,但它包含有一个程序最重要的部分,那就是我们在几乎所有代码中都能看到的main函数,我们编译成可执行文件并查看符号表,过滤出里面的函数如下(为了方便查看我手动调整了grep

windows平台中让函数在main函数之前执行的方法

1.将要执行的代码写到类的构造函数中,并定义对应的全局变量2.将要执行的代码写到TLS回调函数中在c/c++中,我们都知道main函数是程序开始执行的地方,但是在进行反调试的时候,很多时候都需要调试检测函数在main函数之前执行. 1.将要执行的代码写到类的构造函数中,并定义对应的全局变量在windows平台中,执行我们手写的main函数之前,系统会执行一段CRTstartup代码,对系统的堆栈.全局变量.命令行参数.环境变量等进行初始化操作.该方法就是利用windows在执行main函数之前先