3.1.2 段的定义
1. 段的概念
把上面的Win32的Hello World源程序中的语句归纳精简一下,再列在下面:
.386
.model flat,stdcall
option casemap:none
<一些include语句>
.data
<一些字符串、变量定义>
.code
<代码>
<开始标号>
<其他语句>
end 开始标号
上一节讲到的选项、模式等定义并不会在编译好的可执行程序中产生什么东西,它们只是“说明”,而真正的数据和代码是定义在各个段中的,如上面的 .data段和 .code段,考虑到不同的数据类型,还可以有其他种类的数据段,下面是包含全部段的源程序结构:
.386
.model flat,stdcall
option casemap:none
<一些include语句>
.stack [堆栈段的大小]
.data
<一些初始化过的变量定义>
.data?
<一些没有初始化过的变量定义>
.const
<一些常量定义>
.code
<代码>
<开始标号>
<其他语句>
end 开始标号
.stack,.data,.data?,.const和 .code是分段伪指令,Win32中实际上只有代码和数据之分,.data,.data?和 .const都是数据段,.code是代码段,与DOS汇编不同,由于Win32汇编不必考虑堆栈,系统会为程序分配一个向下扩展的、足够大的段作为堆栈段,所以 .stack段定义常常被忽略。
前面不是说过Win32环境下不用“段”了吗?是的,这些“段”实际上并不是DOS汇编中那种意义的段,而是内存的“分段”。上一个段的结束就是下一个段的开始,所有的“分段”合起来,包括系统使用的地址空间,就组成了整个可以寻址的4 GB空间。由于Win32环境的内存管理使用了80386处理器的分页机制,每个页(4 KB大小)可以自由指定属性,所以上一个4 KB可能是代码,属性是可执行但不可写,下一个4 KB就有可能是既可读也可写但不可执行的数据,再下面呢?有可能是可读不可写也不可执行的数据。Win32汇编源程序中“分段”的概念实际上是把不同类型的数据或代码归类,再放到不同属性的内存页(也就是不同的“分段”)中,这中间不涉及使用不同的段选择器。虽然使用和DOS汇编同样的
.code和 .data语句来定义,意思可是完全不同了!为了简单起见,在本书中还是简称“段”,读者应该注意到其中不同的含义。
2. 数据段
.data,.data?和 .const定义的是数据段,分别对应不同方式的数据定义,在最后生成的可执行文件中也分别放在不同的节区(Section)中。程序中的数据定义一般可以归纳为3类。
第一类是可读可写的已定义变量。这些数据在源程序中已经被定义了初始值,而且在程序的执行中有可能被更改,如一些标志等,这些数据必须定义在 .data段中,.data段是已初始化数据段,其中定义的数据是可读可写的,在程序装入完成的时候,这些值就已经在内存中了,.data段一般存放在可执行文件的_DATA节区内。
第二类是可读可写的未定义变量。这些变量一般是当做缓冲区或者在程序执行后才开始使用的,这些数据可以定义在 .data段中,也可以定义在 .data?段中,但一般把它放到 .data?段中。虽然定义在这两种段中都可以正常使用,但定义在 .data?段中不会增大 .exe文件的大小。举例说明,如果要用到一个100 KB的缓冲区,可以用下面的语句定义:
szBuffer db 100 * 1024 dup (?)
这个语句如果放在 .data段中,编译器认为这些数据在程序装入时就必须有效,所以它在生成可执行文件的时候保留了所有的100 KB的内容,即使它们是全零!假设程序其他部分的大小是50 KB,那么最后的 .exe文件就会是150 KB大小,如果缓冲区定义为1 MB,那么 .exe文件会增大到1 050 KB。.data?段则不同,其中的内容编译器会认为程序在开始执行后才会用到,所以在生成可执行文件的时候只保留了大小信息,不会为它浪费磁盘空间。在与上面同样的情况下,即使缓冲区定义为1 MB,可执行文件同样只有50 KB!总之,.data?段是未初始化数据段,其中的数据也是可读可写的,但在可执行文件中不占空间,.data?段在可执行文件中一般存放在_BSS节区中。
第三类数据是一些常量。如一些要显示的字符串信息,它们在程序装入的时候也已经有效,但在整个执行过程中不需要修改,这些数据可以放在 .const段中,.const段是常量段,它是可读不可写的。为了方便起见,在小程序中常常把常量一起定义到 .data段中,而不另外定义一个 .const段。在程序中如果不小心用了对 .const段中的数据做写操作的指令,会引起保护错误,Windows会显示一个如图3.2所示的提示框并结束程序。

图3.2 对 .const段写操作引起的非法操作
如果不怕程序可读性不佳的话,把 .const段中定义的东西混到 .code段中去也可以正常使用,因为 .code段也是可以读的。
3. 代码段
.code段是代码段,所有的指令都必须写在代码段中,在可执行文件中,代码段一般是放在_TEXT节区中的。Win32环境中的数据段是不可执行的,只有代码段有可执行的属性。对于工作在特权级3的应用程序来说,.code段是不可写的,在编DOS汇编程序的时候,好事的程序员往往有个习惯,就是靠改动代码段中的代码来做一些反跟踪的事情,如果企图在Win32汇编下做同样的事情,结果就是和上面同样的“非法操作”。
当然事物总有两面性,在Windows 95下,在特权级0下运行的程序对所有的段都有读写的权利,包括代码段。另外,在优先级3下运行的程序也不是一定不能写代码段,代码段的属性是由可执行文件PE头部中的属性位决定的,通过编辑磁盘上的 .exe文件,把代码段属性位改成可写,那么在程序中就允许修改自己的代码段。一个典型的应用就是一些针对可执行文件的压缩软件和加壳软件,如Upx和PeCompact等,这些软件靠把代码段进行变换来达到解压缩或解密的目的,被处理过的可执行文件在执行时需要由解压代码来将代码段解压缩,这就需要写代码段,所以这些软件对可执行文件代码段的属性预先做了修改。
4. 堆栈段
在程序中不必定义堆栈段,系统会自动分配堆栈空间。惟一值得一提的是,堆栈段的内存属性是可读写并且是可执行的,这样靠动态修改代码的反跟踪模块可以拷贝到堆栈中去边修改边执行。一些病毒或者黑客工具用到的缓冲区溢出技术也用到了这个特征,有兴趣了解的读者可以查阅相关的资料。






