1、 Debug模式下,VC++6.0下断点运行,按CTRL+F11可查看汇编代码;另外可以用cl /c /FAs YourCppFile.cpp命令行在同目录生成YourCppFile.asm汇编文件。

2、 Push将32位操作数压入堆栈,esp指向栈顶,故esp减去4(字节=32位,在64位机器上则是8)。记住:esp为栈顶指针,堆栈越高,这个值越小。由于Intel处理器采用小端存储模式,故当PUSH一个寄存器(如EBP)的值0012FFB4时,先存低字节再存高字节,即堆栈从栈顶向下应该是B4FF1200,当然,采用LONG型HEX解析内存时显示的数值将不变,仍是0012FFB4。

3、 CALL的本质相当于PUSH+JMP(压入CALL语句下一条指令地址,然后跳转至CALL指定的函数入口地址),RET的本质相当于POP+JMP(弹出CALL语句下一条指令地址,然后跳转该地址)。

4、 对堆栈的操作指令除了PUSH、POP、CALL、RET以外,还可以是ADD,SUB。在C语言中局部变量是保持在栈里,可以用esp – 4*4的方式在堆栈中分配4个4字节长的整形空间,而不用PUSH4次。

5、 用XOR EAX,EAX来取代MOV EAX,0,这样来实现清零的好处是:速度更快,占用字节更少。

6、 LEA取址运算符,但如下语句:LEA EDI,[EBP - 0CCH]与MOV EDI,EBP - 0CCH在语义上是等同的([X]表示存储器X中的内容),但是后者会导致语法错误,原因是MOV不支持后一个操作数为寄存器减去一个数字的形式

7、 C语言函数调用默认方式(_cdcall):调用方把参数反序(从右到左)压入堆栈,被调用方把堆栈复原(获取参数)。这些参数须对齐到机器字长,32、64位CPU分别对齐到4、8个字节。

8、 右3可知,CALL和RET并不是函数存在的绝对证据,我们可以自行操作堆栈然后使用JMP绝对跳转实现函数调用。由于高级语言对函数调用的规则各不一样,由此产生了调用约定,在windows上有:Pascal方式、WINAPI方式(_stdcall)、C方式(_cdcall)。

9、 Pascal函数调用方式基本用在16位函数库中,现在基本不用。

10、_stdcall函数调用方式规则(Windows API):参数从右到左入栈;被调用的函数在返回前自行清理堆栈(可变参数函数调用除外)。

11、_cdcall函数调用方式规则(C编译器默认):参数从右到左入栈;函数返回后,调用者负责清理堆栈,由此通常生成较大可执行文件

12、不管何种调用方式,返回值都放入EAX寄存器中。

13、_cdcall函数:

 int MyFunction(int a,int b)
 {
     return a+b;
 }  

汇编后代码为:

 int MyFunction(int a,int b)
 {
 0040D430 push   ebp                     ;1)
 0040D431 move   ebp,esp                  ;2)
 0040D433 sub    esp,40h                 ;3)
 0040D436 push   ebx
 0040D437 push   esi
 0040D438 push   edi                     ;4)
 0040D439 lea    edi,[ebp-40h]
 0040D43C mov    ecx,10h
 0040D441 mov    eax,0CCCCCCCCh
 0040D446 rep stos dword ptr [edi]       ;5)
     return a+b;
 ]
 0040D44B add    eax,dword ptr [ebp+0Ch] ;6)
 }
 0040D44E pop    edi
 0040D44F pop    esi
 0040D450 pop    ebx                     ;7)
 0040D451 mov    esp,ebp
 0040D453 pop    ebp                     ;8)
 0040D454 ret      

它所做的事情有:

1) 保存EBP(基址寄存器)。保存之前的ESP值,在返回时恢复,使函数对堆栈能够实现正确操作。

2) 保存ESP到EBP,此时两者相等,都是函数调用以后的栈基址(栈顶)。

3) 在栈中腾出40H的字节区域用来保存局部变量。大小是可变的,由编译器自动分配。

4) 保存ebx,esi,edi到堆栈,函数调用完后恢复。

5) 初始化局部变量区域,循环10H次,每次赋值4个字节(共10H * 4 = 40H字节,与上面分配的字节区域刚好对应)为0CCCCCCCCH,这个值实际上是INT3的机器码,是一个断点中断指令,因局部变量区域不可能被执行,如果执行了则报错。

6) 第一个参数获取位置为ebp + 双倍CPU字长,这里是ebp + 8,后面的依据类型进行偏移。需要说明一点的是,调用者在调用前将参数倒序压入堆栈,所有参数压入以后,在执行CALL指令时,它会自动将CALL指令下面的一条指令地址压入堆栈;此外,进入调用的函数以后,第一件事就是压入EBP到堆栈。由此可以看出,函数当前栈顶(ESP指向位置)与最后一个压入栈的参数(参数列表的第一个参数)相隔了两个CPU字长。这里是4 * 2 =8个字节,故为ebp + 8。

7) 恢复ebx,esi,edi。这里是与入口对应的现场恢复,没什么好说的

8) EBP是被调用者栈顶指针,其内存单元的值是调用者栈顶指针。所以,这里一方面是使ESP指向被调用者的栈顶(也是调用者的栈底),另一方面是恢复调用者的栈基址。

9) RET执行函数返回,此时,ESP自动加上一个CPU字长,指向最后一个被压入的参数位置。

10)  函数的返回值在EAX中,这里不需要额外操作,如果结果不是在EAX,则在返回前一定有MOV操作(或其等同效果的操作)

14、对上面的程序执行如下调用:

 int main(void)
 {
     MyFunction(,);
     ;
 }  

其汇编代码为:

 int main(void)
 {
 0040D3F0 push   ebp             ;a)
 0040D3F1 mov    ebp,esp
 0040D3F3 sub    esp,40h             ;b)
 0040D3F6 push   ebx             ;c)
 0040D3F7 push   esi             ;d)
 0040D3F8 push   edi             ;e)
 0040D3F9 lea    edi,[ebp-40h]
 0040D3FC mov    ecx,10h
 0040D401 mov    eax,0CCCCCCCCh
 0040D406 rep stos dword ptr [edi]
     MyFunction(,);
                ;f)
                ;g)
 (MyFunction) (0040100a)   ;h)  

     return ;
 0040D414 xor    eax,eax
 }
 0040D416 pop    edi
 0040D417 pop    esi
 0040D418 pop    ebx
 0040D419 add    esp,40h
 0040D41C cmp    ebp,esp
 0040D41E call   __chkesp (0040d460)
 0040D423 mov    esp,ebp
 0040D425 pop    ebp
 0040D426 ret  

对以上代码执行时的内存单元数据情况如下表:

操作
堆栈地址 HEX数据 描述 EBP寄存器
     
4) 0012FED8 0012FF80 MyFunction函数压入edi 0012FF24
4) 0012FEDC 01F8BCE8 MyFunction函数压入esi 0012FF24
4) 0012FEE0 7FFDF000 MyFunction函数压入ebx 0012FF24
3)、5)

0012FEE4

0012FF20

CCCCCCCC

CCCCCCCC

MyFunction函数压入预留局部变量存储区,全部初始化为0xCCCCCCCCH 0012FF24
1)、2) 0012FF24 0012FF80 MyFunction函数压入ebp 0012FF24
h) 0012FF28 0040D411 mian函数CALL调用,压入返回地址 0012FF80
g) 0012FF2C 00000001 参数1入栈 0012FF80
d) 0012FF30 00000002 参数2入栈 0012FF80
e) 0012FF34 01DD0000 main函数压入edi 0012FF80
d) 0012FF38 01F8BCE8 main函数压入esi 0012FF80
c) 0012FF3C 7FFDF000 main函数压入ebx 0012FF80
b)

0012FF40

0012FF7C

CCCCCCCC

CCCCCCCC

main函数压入预留局部变量存储区,全部初始化为0xCCCCCCCCH 0012FF80
a) 0012FF80 0012FFC0 main函数压入ebp 0012FF80

函数地用过程中的操作由下往上看,注意CALL与RET指令执行时的堆栈变化。其中红色和蓝色为EBP寄存器变化情况,灰绿色为参数压栈。

15、 需要注意的是,以上的汇编代码来自于VC++6.0编译器,在自行写汇编或者嵌入汇编时要稍做改变。比如说CALL指令直接写为call MyFunction

《天书夜读:从汇编语言到windows内核编程》一 汇编指令与C语言的更多相关文章

  1. 《天书夜读:从汇编语言到windows内核编程》三 练习反汇编C语言程序

    1) Debug版本算法反汇编,现有如下3×3矩阵相乘的程序: #define SIZE 3 int MyFunction(int a[SIZE][SIZE],int b[SIZE][SIZE],in ...

  2. 《天书夜读:从汇编语言到windows内核编程》五 WDM驱动开发环境搭建

    (原书)所有内核空间共享,DriverEntery是内核程序入口,在内核程序被加载时,这个函数被调用,加载入的进程为system进程,xp下它的pid是4.内核程序的编写有一定的规则: 不能调用win ...

  3. 《天书夜读:从汇编语言到windows内核编程》八 文件操作与注册表操作

    1)Windows运用程序的文件与注册表操作进入R0层之后,都有对应的内核函数实现.在windows内核中,无论打开的是文件.注册表或者设备,都需要使用InitializeObjectAttribut ...

  4. 《天书夜读:从汇编语言到windows内核编程》六 驱动、设备、与请求

    1)跳入到基础篇的内核编程第7章,驱动入口函数DriverEnter的返回值决定驱动程序是否加载成功,当打算反汇编阅读驱动内核程序时,可寻找该位置. 2)DRIVER_OBJECT下的派遣函数(分发函 ...

  5. 《天书夜读:从汇编语言到windows内核编程》十一 用C++编写内核程序

    ---恢复内容开始--- 1) C++的"高级"特性,是它的优点也是它的缺点,微软对于使用C++写内核程序即不推崇也不排斥,使用C++写驱动需注意: a)New等操作符不能直接使用 ...

  6. 《天书夜读:从汇编语言到windows内核编程》四 windows内核调试环境搭建

    1) 基础篇是讲理论的,先跳过去,看不到代码运行的效果要去记代码是一个痛苦的事情.这里先跳入探索篇.其实今天的确也很痛苦,这作者对驱动开发的编译与调试环境介绍得太模糊了,我是各种尝试,对这个环境的搭建 ...

  7. 《天书夜读:从汇编语言到windows内核编程》十 线程与事件

    1)驱动中使用到的线程是系统线程,在system进程中.创建线程API函数:PsCreateSystemThread:结束线程(线程内自行调用)API函数:PsTerminateSystemThrea ...

  8. 《天书夜读:从汇编语言到windows内核编程》九 时间与定时器

    1)使用如下自定义函数获取自系统启动后经历的毫秒数:KeQueryTimeIncrement.KeQueryTickCount void MyGetTickCount(PULONG msec) { L ...

  9. 《天书夜读:从汇编语言到windows内核编程》七 内核字符串与内存

    1)驱动中的字符串使用如下结构: typedef struct _UNICODE_STRING{ USHORT Length; //字符串的长度(字节数) USHORT MaximumLength; ...

  10. 《天书夜读:从汇编语言到windows内核编程》二 C语言的流程与处理

    1) Debug与Release的区别:前者称调试版,后者称发行版.调试版基本不优化,而发行版会经过编译器的极致优化,往往与优化前的高级语言执行流程会大相径庭,但是实现的功能是等价的. 2) 如下fo ...

随机推荐

  1. Nunit测试工具使用实例

    前言: 本文主要是介绍了Nunit的基本使用,其中参详了很多已有的文章,由于最近要使用其进行测试,所以对网上的文章做了下整理,同时加入了一些自己的实践. NUnit的属性 TestFixture 它标 ...

  2. NPOI导出WPF DataGrid控件显示数据

    最近做个项目,需要导出DataGrid显示的数据,中间遇到了不少的坑,在此纪录一下,方便以后查看,也希望能给用到的人,一点帮助. 导出DataGrid显示的数据,并不是导出DataGrid的Items ...

  3. Django扩展自定义manage命令

    使用django开发,对python manage.py ***命令模式肯定不会陌生.比较常用的有runserver,migrate... 本文讲述如何自定义扩展manage命令. 1.源码分析 ma ...

  4. linux命令和awk

    1.统计一下代码量 find . -name "*.py" | xargs wc -l | awk 'BEGIN {size = 0} { size+=$1} END{print ...

  5. sa账户和密码丢失如何找回

    来自:http://www.cnblogs.com/xred/archive/2012/03/09/2386185.html 在网上看了很多如何修改SQLServer2005的密码的方法.大多数都是转 ...

  6. WPF 设置输入只能英文

    有时输入只能让用户输入英文,那么如何设置输入只能英文? 首先在xaml 写一个 TextBox ,给他一个名字. <TextBox x:Name="txt"></ ...

  7. JS中的类型识别

    JS为弱类型语言,所以类型识别对JS而言尤为重要,JS中常用的类型识别方法有4种:typeof.Object.prototype.toString.constructor和instanceof. (1 ...

  8. HashMap工作原理 和 HashTable

    原文链接: Javarevisited 翻译: ImportNew.com - 唐小娟 译文链接: http://www.importnew.com/7099.html 你用过HashMap吗 譬如H ...

  9. (转)log4j使用介绍

    原文出自: log4j使用介绍 日志是应用软件中不可缺少的部分,Apache的开源项目Log4j是一个功能强大的日志组件,提供方便的日志记录.以下是个人经验,具体请参考Log4j文档指南. Log4j ...

  10. Python3爬虫登录模拟

    使用Python爬虫登录系统之后,能够实现的操作就多了很多,下面大致介绍下如何使用Python模拟登录. 我们都知道,在前端的加密验证,只要把将加密环境还原出来,便能够很轻易地登录. 首先分析登录的步 ...