Akkuman 的博客

习技术,不积跬步无以至千里,不积小流无以成江海

Win32 汇编学习 (1):基本概念

背景知识

Windows 把每一个 Win32 应用程序放到分开的虚拟地址空间中去运行,也就是说每一个应用程序都拥有其相互独立的 4GB 地址空间,当然这倒不是说它们都拥有 4GB 的物理地址空间,而只是说能够在 4GB 的范围内寻址。操作系统将会在应用程序运行时完成 4GB 的虚拟地址和物理内存地址间的转换。这就要求编写应用程序时必须格守 Windows 的规范,否则极易引起内存的保护模式错误。而过去的 Win16 内存模式下,所有的应用程序都运行于同一个 4GB 地址空间,它们可以彼此 "看" 到别的程序的内容,这极易导致一个应用程序破坏另一个应用程序甚至是操作系统的数据或代码。

和 16 位 Windows 下的把代码分成 DATA,CODE 等段的内存模式不同,WIN32 只有一种内存模式,即 FLAT 模式,意思是 "平坦" 的内存模式,再没有 64K 的段大小限制,所有的 WIN32 的应用程序运行在一个连续、平坦、巨大的 4GB 的空间中。这同时也意味着您无须和段寄存器打交道,您可以用任意的段寄存器寻址任意的地址空间,这对于程序员来说是非常方便的,这也使得用 32 位汇编语言和用 C 语言一样方便。 在 Win32 下编程,有许多重要的规则需要遵守。有一条很重要的是:Windows 在内部频繁使用 ESI,EDI,EBP,EBX 寄存器,而且并不去检测这些寄存器的值是否被更改,这样当您要使用这些寄存器时必须先保存它们的值,待用完后再恢复它们,一个最显著的应用例子就是 Windows 的 CallBack 函数中。

内容

一般的 Win32 汇编都有下面的程序段,这是一个 Win32 汇编编程的基础框架,若您现在还不知道这些指令的确切意义的话,没关系, 随后我就会给大家详细解释。

.386 
.MODEL Flat, STDCALL 
.DATA 
    <Your initialized data> 
    ...... 
.DATA? 
   <Your uninitialized data> 
   ...... 
.CONST 
   <Your constants> 
   ...... 
.CODE 
   <label> 
    <Your code> 
   ..... 
    end <label>

这就是一般 Win32 汇编编程的基础框架,其中各个关键词的解释说明如下:

.386

这是一个汇编语言伪指令,他告诉编译器我们的程序是使用 80386 指令集编写的。您还可以使用 .486、.586, 但最安全的还是使用. 386。对于每一种 CPU 有两套几乎功能相同伪指令: .386/.386P、 486/.486P、 586/.586P。 带 P 的指令标明您的程序中可以用特权级指令。特权级指令是保留给操作系统的,如虚拟设备驱动程序。在大多数时间,您的程序都无须运行在 RING0 层,故用不带后缀 P 的伪指令已足够了。

.MODEL FLAT,STDCALL

.MODEL 是用来指定内存模式的伪指令,在 Win32 下,只有一种内存模型,那就是 FLAT。 STDCALL 告诉编译器参数的传递约定。参数的传递约定是指参数传达时的顺序 (从左到右或从右到左) 和由谁恢复堆栈指针(调用者或被调用者)。在 Win16 下有两种约定:CPASCAL。C 约定规定参数传递顺序是从右到左,即最右边的参数最先压栈,由调用者恢复堆栈指针。

例如:为调用函数 foo ( int first_param, int second_param, int third_param ); 按 C 约定的汇编代码应该是这样的:

push [third_param]
push [second_param]
push [first_param]
call foo
add esp, 3 * 4 ;调用者自己恢复堆栈指针

PASCAL约定和C约定正好相反,它规定参数是从左向右传递,由被调用者恢复堆栈。Win16 采用了PASCAL约定, 因为PASCAL约定产生的代码量要小。当不知道参数的个数时,C约定特别有用。如在函数wsprintf () 中, wsprintf预先并不知道要传递几个参数,所以它不知道如何恢复堆栈。STDCALLC约定和PASCAL约定的混合体,它规定参数的传递是从右到左,恢复堆栈的工作交由被调用者。Win32 只用STDCALL约定,但除了一个特例,即:wsprintf

.DATA 
.DATA? 
.CONST 
.CODE

上面的四个伪指令是 "分段"(SECTION) 伪指令。我们上面刚讲过 Win32 下没有 "段"(SEGMENT) 的概念,但是您可以把您的程序分成不同的 "分段", 一个 "分段" 的开始即是上一个 "分段" 的结束。WIN32 中只有两种性质的 "分段":DATACODE

其中 DATA"分段" 又分为三种:

  • .DATA 其中包括已初始化的数据。
  • .DATA? 其中包括未初始化的数据。比如有时您仅想预先分配一些内存但并不想指定初始值。使用未初始化的数据的优点是它不占据可执行文件的大小,如:若您要在 .DATA? 段中分配 10,000 字节的空间,您的可执行文件的大小无须增加 10,000 字节,而仅仅是要告诉编译器在装载可执行文件时分配所需字节。
  • .CONST 其中包括常量定义。这些常量在程序运行过程中是不能更改的。 应用程序并不需要以上所有的三个 "分段", 可以根据需要进行定义。
  • .CODE 这是代码 "分段"。

    实际上,分段并不是象在 Dos 下一样,为不同的段分别指出不同的段寄存器,因为 Windows 下只有一个 4GB 的段,Windows 程序中的分段表现在当程序装载时,赋予不同的分段不同的属性,比如说当你的程序加载时,对于 Ring3 程序来说,.code 段是不可写的,而 .data 段是可写的,如果你尝试象在 Dos 下一样写自己的代码部分,你会得到一个蓝屏错误

<label> 
end <label>

是用来唯一标识您的代码范围的标签, 两个标签必须相同,应用程序的所有可执行代码必须在两个标签之间。

留下你的脚步