浅析VS2010反汇编

第一篇

1. 怎样进行反汇编

在调试的环境下,我们能够很方便地通过反汇编窗体查看程序生成的反汇编信息。

例如以下图所看到的。

image

记得中断程序的运行,不然看不到反汇编的指令

image

看一个简单的程序及其生成的汇编指令

复制代码
#include<stdio.h>
#include<windows.h>
const long Lenth=5060000/5;
int main(){
    while(true){
        for(long i=0;i<Lenth;i++){
            ;
        }
        Sleep(10);
    }
}
复制代码

汇编窗体

image


2.  预备知识

函数调用大家都不陌生,调用者向被调用者传递一些參数,然后运行被调用者的代码,最后被调用者向调用者返回结果,还有大家比較熟悉的一句话。就是函数调用是在栈上发生的,那么在计算机内部究竟是怎样实现的呢?
 
对于程序,编译器会对其分配一段内存。在逻辑上能够分为代码段。数据段,堆,栈
代码段:保存程序文本。指令指针EIP就是指向代码段。可读可运行不可写
数据段:保存初始化的全局变量和静态变量,可读可写不可运行
BSS:未初始化的全局变量和静态变量
堆(Heap):动态分配内存,向地址增大的方向增长。可读可写可运行
栈(Stack):存放局部变量。函数參数,当前状态。函数调用信息等,向地址减小的方向增长。很很重要,可读可写可运行
如图所看到的
寄存器
EAX:累加(Accumulator)寄存器,经常使用于函数返回值
EBX:基址(Base)寄存器。以它为基址訪问内存
ECX:计数器(Counter)寄存器,经常使用作字符串和循环操作中的计数器
EDX:数据(Data)寄存器,经常使用于乘除法和I/O指针
ESI:源变址寄存器
DSI:目的变址寄存器
ESP堆栈(Stack)指针寄存器。指向堆栈顶部
EBP基址指针寄存器,指向当前堆栈底部
EIP指令寄存器,指向下一条指令的地址

第二篇

一、VS反汇编方法

1、调出反汇编窗体。

2、调用寄存器窗体(仅仅有在反汇编下才可见)

假设在调试状态还是没有此菜单项。可试着以下操作:

在VS中点击“工具”->“导入和导出设置”,选择“重置全部设置”,下一步。这时你能够保存当前设置或不保存,我觉得无所谓,下一步,选择“Visual C**开发设置”。“完毕”。

这样,“调试”->“窗体”->“寄存器”菜单项应该用显示出来了,记得要确保你的程序是在调试的过程中。

3、查看内存

点击“调试”->“窗体”->“内存”->“内存1”...“内存4”(选一个就能够了。)。在内存窗体中的“地址”栏输入地址,按回车就可以看到该地地址处的内存信息。

 二、经常使用汇编指令介绍

1、经常使用指令

为了照应到没学过汇编程序的同志们,这里简介一下常见的几种汇编指令。

A、add:加法指令。第一个是目标操作数。第二个是源操作数,格式为:目标操作数 = 目标操作数 + 源操作数。

B、sub:减法指令。格式同 add;

C、call:调用函数,一般函数的參数放在寄存器中;

D、ret:跳转会调用函数的地方。

相应于call,返回到相应的call调用的下一条指令。若有返回值。则放入eax中;

E、push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节),esp通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与Windows下的堆栈是不同的,请參考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多,esp也就越来越小

F、pop:与push相反,esp每次加4(字节),一个数据出栈。pop的參数通常是一个寄存器,栈顶的数据被弹出到这个寄存器中。

一般不会把sub、add这样的算术指令,以及call、ret这样的跳转指令归入堆栈相关指令中。可是实际上在函数參数传递过程中,sub和add最经常使用来操作堆栈;call和ret对堆栈也有影响。

 

G、mov:数据传送。第一个參数是目的操作数,第二个參数是源操作数,就是把源操作数复制到目的一份。

H、xor:异或指令,这本身是一个逻辑运算指令,但在汇编指令中一般会见到它被用来实现清零功能。

              用 xor eax,eax这样的操作来实现 mov eax,0。能够使速度更快,占用字节数更少。

I、lea:取得第二个參数地址后放入到前面的寄存器(第一个參数)中

               然而lea也相同能够实现mov的操作,比如:

                                  lea edi,[ebx-0ch]

方括号表示存储单元。也就是提取方括号里的数据所指向的内容,然而lea提取内容的地址,这样就实现了把(ebx-0ch)放入到了edi中,可是mov指令是不支持第二个操作数是一个寄存器减去一个数值的

 

J、stos:串行存储指令。它实现把eax中的数据放入到edi所指的地址中。同一时候edi后移4个字节,这里的stos实际上相应的是stosd,其它的还有stosb,stosw分别相应1,2个字节。

K、jmp:无条件跳转指令,相应于大量的条件跳转指令。

L、jg:条件跳转。大于时成立。进行跳转。通常条件跳转之前会有一条比較指令(用于设置标志位)。

M、jl:小于时跳转

N、jge:大于等于时跳转

O、cmp:比較大小指令。结果用来设置标志位。

P 。rep 依据ECX寄存器的值进行反复循环操作

注:

mov ax,[bx]
[ ]表示是间接寻址。bx和[bx]的差别是。前者操作数就是bx中存放的数,后者操作数是以bx中存放的数为地址的单元中的数。比方bx中存放的数是40F6H,40F6H、40F7H两个单元中存放的数是22H、23H,则
mov ax,[bx]。2223H传送到ax中
mov ax,bx。40F6H传送到ax中


ILT是INCREMENTAL LINK TABLE的缩写。这个@ILT事实上就是一个静态函数跳转的表。它记录了一些函数的入口然后跳过去,每一个跳转jmp占一个字节,然后就是一个四字节的内存地址,加起为五个字节
比方代码中有多处地方调用boxer函数,别处的调用也通过这个ILT表的入口来间接调用,而不是直接call 该函数的偏移。这样在编译程序时。假设boxer函数更新了,地址变了,仅仅须要改动跳表中的地址就能够。有利于提高链接生成程序的效率。

这个是用在程序的调试阶段。当编译release程序时。就不再用这样的方法。


我试着将HEX数据改成00 00。相应的汇编指令变成了add byte ptr [eax],al ,反过来,假设将一个地方的汇编指令改成add byte ptr [eax],al ,相应的HEX数据就成了00 00,也就是说,他们是一一相应的。编译器觉得。00 00 这样两个字节宽度的二进制数相应的汇编指令就是add byte ptr [eax],al


dword 双字 就是四个字节
ptr pointer缩写 即指针

2  、函数參数传递方式

函数调用规则指的是调用者和被调用函数间传递參数及返回參数的方法,在Windows上,经常使用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。

A、_cdecl C调用规则:

(a)參数从右到左进入堆栈

(b)在函数返回后,调用者要负责清除堆栈。这样的调用方式一般会生成较大的可运行程序。

B、_stdcall又称为WINAPI,调用规则例如以下:

(a)參数从右到左进入堆栈。

(b)被调用的函数在返回前自行清理堆栈,这样的方式生成的代码比cdecl小

C、Pascal调用规则(主要用于Win16函数库中。如今基本不用):

(a)參数从左到右进入堆栈;

(b)被调用的函数在返回前自行清理堆栈。

(c)不支持可变參数的函数调用

第三篇

了解反汇编的一些小知识对于我们在开发软件时进行编程与调试大有优点,以下以简介一下反汇编的一些小东西。假设有些解释有问题的地方,希望大家能够指出。

1、新建简单的VC控制台应用程序(对此熟悉的同学能够略过)

A、打开Microsoft Visual Studio 2010,选择主菜单“File”

B、选择子菜单“New”以下的“Project”,打开“New Project”对话框。

C、左边选择Visual C++下的win32,右边选择Win32 Console Application,然后输入一个project名,点击“OK”就可以。在出现的向导中,一切默认。点击Finish就可以。

D、在出现的编辑区域内会出现以你设定的project名命名的CPP文件。内容例如以下:

      #include "stdafx.h"

      int _tmain(int argc, _TCHAR* argv[])

      {

            return 0;

      }

2、VS查看汇编代码

A、VC处于调试状态才干看到汇编指令窗体。因此,能够在 return 0 上设置一个断点:把光标移到 return 0 那一行上。然后按下F9键设置一个断点。

B、按下F5键进入调试状态,当程序停在 return 0 这一行上时,打开菜单“Debug”下的“Windows”子菜单,选择“Disassembly”。

这样,出现一个反汇编的窗体。显示以下的信息:

--- d:/my documents/visual studio 2008/projects/casmtest/casmtest/casmtest_main.cpp 
// CAsmTest.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

int _tmain(int argc, _TCHAR* argv[])
{
00411370  push        ebp  
00411371  mov         ebp,esp 
00411373  sub         esp,0C0h 
00411379  push        ebx  
0041137A  push        esi  
0041137B  push        edi  

0041137C  lea         edi,[ebp-0C0h] 
00411382  mov         ecx,30h 
00411387  mov         eax,0CCCCCCCCh 
0041138C  rep stos    dword ptr es:[edi] 

 return 0;
0041138E  xor         eax,eax 
}
00411390  pop         edi  
00411391  pop         esi  
00411392  pop         ebx  

00411393  mov         esp,ebp 
00411395  pop         ebp  
00411396  ret  

上面就是系统生成的main函数原型,确切的说是_tmain()的反汇编的相关信息。相信学过汇编语言的肯定就能够了解它所做的操作了。


VC中訪问无效变量出错原因

我们看上面主函数反汇编后的当中一段代码例如以下:

0041137C  lea         edi,[ebp-0C0h] 
00411382  mov         ecx,30h 
00411387  mov         eax,0CCCCCCCCh 
0041138C  rep stos    dword ptr es:[edi]

从代码的表面上看。它是实现把从ebp-0C0h開始的30h个字的空间写入0CCCCCCCCh。当中eax为四位的数据,这样能够计算:

                      0C0h = 30h * 4

也就是把从ebp-0C0h 到ebp之间的空间初始化为0CCCCCCCCh。大家在学习反汇编的过程中会发现。事实上编译器会依据情况把相应长度的这样一段作为局部变量的空间,而这里把局部变量区域全都初始化成0CCCCCCCCh也是有其用意的,做VC编程的工作者,特别是刚開始学习的人可能不会对0CCCCCCCCh这个常量陌生。

0cch实际上是int 3指令的机器码,这是一个断点中断指令(在反编译出的信息中大家会看到int 3),因为局部变量不可被运行,或者假设在没有初始化的时候进行了訪问,则就会出现訪问失败错误。这个在VC编译Debug版本号中才干看到提示这个错误,在Release版本号中,会以第二种错误形式体现。

以下,我们改动主程序看下new与delete的反汇编的效果(凝视直接加到反汇编的代码中了)。

VC生成project,写入源码例如以下:

(1)情况1

// ASM_Test.cpp : Defines the entry point for the console application.                    (  源码1 )
//
#include "stdafx.h"
#include "stdlib.h"
int _tmain(int argc, _TCHAR* argv[])
{
    int *pTest = new int(3);                //定义一个整型指针,并初始化为 3
    printf( "*pTest = %d/r/n", *pTest );    //调用库函数printf输出数据
    delete []pTest;                            //删除这个指针

    return 0;
}
这里仅仅看下在new与delete进行空间管理时进行反汇编时可能出现的一些情况。我们把上面源码称为源码(1),我们依照前面解说的查看VS下反汇编的方法能够看到相应于上面代码的反汇编代码例如以下:
--- f:/mysource/asm_test/asm_test/asm_test.cpp ---------------------------------                      ( 反汇编代码 1)
// ASM_Test.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "stdlib.h"

int _tmain(int argc, _TCHAR* argv[])
{

;(1)函数预处理部分
004113C0  push        ebp  
004113C1  mov         ebp,esp ;保存堆栈的栈顶位置
004113C3  sub         esp,0E8h ;要置为0CCCCCCCCh 保留变量空间长度
004113C9  push        ebx       ;保存寄存器ebx、esi、edi
004113CA  push        esi  
004113CB  push        edi  
004113CC  lea         edi,[ebp-0E8h]    ;提出要置为0CCCCCCCCh 的空间起始地址
004113D2  mov         ecx,3Ah      ;要置为0CCCCCCCCh 的个数,每一个占4个字节
004113D7  mov         eax,0CCCCCCCCh  ;于是3Ah * 4 = 0E8h
004113DC  rep stos    dword ptr es:[edi]  ;进行置为0CCCCCCCCh操作


(2)定义一个int 型指针。分配空间后。并初始化为 3 。

    int *pTest = new int(3);                //定义一个整型指针,并初始化为 3
004113DE  push        4    ;要分配的空间长度,会依据定义的数据类型而不同
004113E0  call        operator new (411186h)   ;分配空间。并把分配空间的起始地址放入eax中
004113E5  add         esp,4    ;因为new与delete函数本身没有对栈进行弹出操作,所以,要编写者自己处理
004113E8  mov         dword ptr [ebp-0E0h],eax  ;比較分配的空间是否为0,假设为0 
004113EE  cmp         dword ptr [ebp-0E0h],0 
004113F5  je          wmain+51h (411411h) 
004113F7  mov         eax,dword ptr [ebp-0E0h]      ;对于分配的地址分配空间进行赋值为:3
004113FD  mov         dword ptr [eax],3 
00411403  mov         ecx,dword ptr [ebp-0E0h] 
00411409  mov         dword ptr [ebp-0E8h],ecx   ;似乎用[ebp - 0E0h]和[ebp - 0E8h]作为了中间存储单元
0041140F  jmp         wmain+5Bh (41141Bh) 
00411411  mov         dword ptr [ebp-0E8h],0     ;上面分配空间失败是的操作
0041141B  mov         edx,dword ptr [ebp-0E8h] 
00411421  mov         dword ptr [pTest],edx           ;数据最后送入pTest变量中

;调用printf函数进行数据输出
    printf( "*pTest = %d/r/n", *pTest );    //调用库函数printf输出数据
00411424  mov         esi,esp   ;用于调用printf后的Esp检測。不明确编译器为什么这样做
00411426  mov         eax,dword ptr [pTest]   ;提取要打印的数据。先是地址。以下一条是提取详细数据
00411429  mov         ecx,dword ptr [eax] 
0041142B  push        ecx         ;两个參数入栈
0041142C  push        offset string "*pTest = %d/r/n" (41573Ch) 
00411431  call        dword ptr [__imp__printf (4182C4h)]      ;调用函数
00411437  add         esp,8         ;因为库函数无出栈管理操作,同new与delete。所以要加 8,进行堆栈处理
0041143A  cmp         esi,esp        ;对堆栈的栈顶进行測试
0041143C  call        @ILT+325(__RTC_CheckEsp) (41114Ah) 

;进行指针变量的清理工作
    delete []pTest;                            //删除这个指针
00411441  mov         eax,dword ptr [pTest]   ;[pTest] 中放入的是分配的地址,以下几条指令转悠一圈
00411444  mov         dword ptr [ebp-0D4h],eax   ;就是要把要清理的地址送入堆栈,然后调用delete函数
0041144A  mov         ecx,dword ptr [ebp-0D4h] 
00411450  push        ecx  
00411451  call        operator delete (411091h) 
00411456  add         esp,4     ;对堆栈进行处理,同new与printf函数

;函数结束后,进行终于的清理工作
    return 0;
00411459  xor         eax,eax   ;做相应的清理工作,堆栈中保存的变量送回原寄存器
}
0041145B  pop         edi  
0041145C  pop         esi  
0041145D  pop         ebx  
0041145E  add         esp,0E8h       ;进行堆栈的栈顶推断
00411464  cmp         ebp,esp 
00411466  call        @ILT+325(__RTC_CheckEsp) (41114Ah) 
0041146B  mov         esp,ebp 
0041146D  pop         ebp  
0041146E  ret  

--- No source file -------------------------------------------------------------;后面不再是源码


在列出反汇编程序时把反汇编代码的上下的分解凝视也列了出来,亲手去查看的朋友可能会发如今这段代码的之外的其它部分会有大量的int 3汇编中的中断指令,这个是与上面的所说的0CCCCCCCCh具有一致性,这些区域是无效区域,但代码訪问这些区域时就会出现非法訪问提示

当然,你应该能够想到。那个提示是能够被屏蔽掉的,你能够把这部分区域填充上数据或者改动 iint 3 调用的中断程序。

从以上反汇编程序,我们能够发现几点:

A、一些内部的库函数是不会对堆栈进行出栈管理的,所以若要对反汇编程序进行操作时,一点要注意这一点

B、编译器会自己主动的加上一些对栈顶的检查工作,这个是我们在做VC调试时经常遇到的一个问题。就是堆栈错误

当然以上仅仅是对debug版本号下的程序进行反汇编,假设为release 版本号,代码就会进行大量的优化,在理解时会有一定的难度。有兴趣朋友能够试着反汇编一下,推荐大家有IDA返回工具。感觉挺好用的。






posted on 2017-05-17 12:21  yjbjingcha  阅读(5017)  评论(0编辑  收藏  举报

导航