Assembly

对一个简单的 win32 汇编程序剖析及拓展

zeronohacker · 8月6日 · 2018年 · 40次已读

1 一个简单的 win32 汇编程序

.386
.model flat,stdcall
option casemap:none

; Include 文件定义
inclue        windows.inc
include       user32.inc
include       kernel32.inc
includelib    user32.lib
includelib    kernel32.lib

; 数据段
.data
szCaption db 'A MessageBox !',0
szText db 'Hello, World !',0

; 代码段
.code
start:
    invoke MessageBox,NULL,offset szText,\
            offset szCaption,MB_OK
    invoke ExitProcess,NULL
    end start

2 指定指令集

.386 语句是汇编程言的伪指令,类似的还有 .8086,.186,.286,.386/.386p,.486/.486p 和 .586/.586p 等,用于告诉编译器在本程序中使用的指令集。在 DOS 汇编默认使用的是 8086 指令集,如果在源程序中写入 80386 所特有的指令或使用 32 位的寄存器就会报错,为了在 DOS 环境下进行保护模式编程或仅为了使用 32 位寄存器,常在 DOS 的汇编中使用 .386 来定义。Win32 环境工作在 80386 及以上的处理器中,所以 .386 这一句必不可少。后面带 p 的伪指令则表示程序中可以使用特权指令。

3 .model 语句

.model 语句在低版本的宏汇编中已经存在,用来定义程序工作的模式,它的使用方法是:

.model 内存模式[,语言模式][,其他模式]

内存模式的定义影响最后生成的可执行文件,现在一般都用 flat (平坦模式),可以突破 64KB 段大小的限制,在保护模式下,系统把每一个 win32 程序都放在分开的虚拟地址空间中去运行,也就是说,每一个程序都拥有其相互独立的 4GB 地址空间,4GB 的空间用 32 位寄存器全部都能访问到,相关模式可参看下图:

在 flat 后,便是语言模式,即子程序的调用方式,开头的示例中所用的是 stdcall,它指出了调用子程序或 win32 API 时参数传递的次序和堆栈平衡的方法。windows 的 API 调用使用的是 stdcall 格式,所以在 win32 汇编中没有选择,必须在 .model 中加上 stdcall 参数。

4 option 语句

用 option 语句定义的选项非常多,比如 option language 定义和 option segment 定义等,在 win32 汇编中,只需要定义 option casemap:none,这个语句定义了程序中的变量和子程序名是否对大小写敏感,由于 win32 API 中的 API 名称是区分大小写的,所以必须指定这个选项,否则在调用 API 的时候会有问题。

5 段的概念

在 win32 汇编程序中,经常会看到 .stack,.data,.data?,.const,.code 等段说明,其中 .stack 常常被忽略,因为 win32 汇编程序不必考虑堆栈,系统会为程序分配一个向下扩展的、足够大的段作为堆栈段,这一点与 DOS 汇编 (16 位) 不同。注意这里的段不是 DOS 汇编中那种意义上的段,而是内存 “分段”,上一段的结束就是下一段的开始,所有的 “分段” 合起来,包括系统使用的地址空间,就组成了整个可以寻址的 4GB 空间,win32 环境的内存管理使用了 80386 处理器的分页机制,每页 (4KB) 可以自由指定属性,所以上一个 4KB 可能是代码,属性是可执行但不可写,下一个 4KB 就有可能是既可读也可写但不执行的数据,再下面呢?有可能是可读不可写也不可执行的数据。可以换个角度看,程序中的 “分段” 的概念实际上是把不同类型的数据或代码归类,再放到不同属性的内存页中,这中间不涉及使用不同的段选择器。

.data,.data? 和 .const 定义的是数据段,分别对应不同方式的数据定义,在最后生成可执行文件中也分别放在不同的节区中,有三类数据:

  • 第一类是可读可写的已定义变量,必须定义在 .data 段,.data 段是已初始化数据段,其中定义的数据是可读可写的,.data 段一般存放在可执行文件的 _DATA 节区内;
  • 第二类是可读可写的未定义变量,这些变量一般是当作缓冲区或者在程序执行后才开始使用,可定义在 .data 段,也可定义在 .data? 段,但最是定义在 .data? 段,因为生成后,后者比前者体积更大,.data? 段一般存放在 _BSS 节区中;
  • 第三类数据是一些常量,此类数据可定义在 .const 段中,它是可读不可写的,你也可以定义到 .data 段中,但最好还是选择 .const 段中;

.code 段为代码段,代码段一般放在 _TEXT 节区中,可读不可写可执行,一定不可写吗?这可不一定,比如一些压缩软件和加壳软件,它们就可以修改程序的代码段,另外在 0 环运行的程序对所有的段都有读写权利,包括代码段。

.stack 段还需提一下,.stack 段是可读可写可执行的,这样靠动态修改代码的反跟踪模块可以拷贝到堆栈中去边修改边执行,一些病毒或黑客工具用到缓冲区溢出技术也用到了这个特征。

6 程序结束和程序入口

程序的结束由 end 语句来指定,end 后这个标号为程序的入口。当编写多模块程序时,不需要在每个程序文件中指定入口,当最终把多个模块链接在一起时,只需在主模块中指定即可。

7 注释和换行

汇编源程序的注释符号为分号,可放在任意位置。换行符号为反斜杠,如开头那个示例那样。

8 调用 API

API 为应用程序接口,为一系列函数、结构和消息等,不仅为应用程序所调用,同时也是 windows 自身的一部分。在 DOS 下,操作系统功能是通过软中断来实现,比如 int 21h 为 DOS 中断,int 13h 为磁盘中断,int 10h 为视频中断,当应用程序要引用系统功能时要把相应的参数放在各个寄存器再调用相应的中断,程序控制权转到中断处理程序去执行,完成后会通过 iret 中断返回指令回到应用程序中,比如:

mov ah,9
mov dx,offset szHello
int 21h

9 为功能号,存放在 ah 中,表示屏幕显示,要输出到屏幕上的内容地址存放在 dx 中,然后调用 int 21h,字符串就会显示在屏幕上,DOS 时代汇编程序员都有一本厚厚的 《中断大全》,因为所有的功能编号包括使用的参数仅从字面上看,是看不出一点头绪来的。另外,80×86 系列处理器最多只能处理 256 个中断,中断数量太少,到最后就会现中断里挂中断,这是非常不好的,所以 API 出现了,win32 系统功能实现放在 Dll 中,我们可以通过 DLL 中的导出表可看出,有三大核心 Dll,分别为 kernel32.dll,gdi32.dll,user32.dll。想一想在 C 语言如何使用一个函数,首先需要先声明,然后就是要这个函数的实现,在 win32 汇编程序中,系统函数声明在 inc 文件中,函数实现在 lib 文件 (静态库) 中,前者由 include 包含,后者由 includelib 包含:

include 文件名
include <文件名>
includelib 文件名
includelib <文件名>

参照下开头那个示例。那如何调用了,这得使用到另一个伪指令 invoke,为什么会有 invoke 指令,因为在 win32 的 API 中,动不动就是十几个参数,甚至更多,如果都用 push 来写的话,这就非常恐怖了,invoke 正好解决这个问题,参数可写在一行,如下:

invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK

invoke 为 MASM 编译器的伪指令,在编译的时候由编译器把上面的指令展开成我们需要的 4 个 push 和 1 个 call 指令:

push uType
push lpCaption
push szText
push hWnd
call MessageBox

函数的返回值存放在 eax 中,如果要返回的内容不是一个 eax 所能容纳的,win32 API 采用的方法一般是 eax 返回一个指向返回数据的指针,或者在调用参数中提供一个缓冲区地址,干脆把数据直接返回到缓冲区中去。

最后提一下在汇编下一个 API 函数的声明,比如 MessageBox:

MessageBox PROTO hWnd:dword,lpText:dword,lpCaption:dword,uType:dword

可简写为:

MessageBox PROTO :dword,:dword,:dword,:dword

句中 PROTO 是函数声明的伪指令,其实是不存在 MessageBox 的,只存在 MessageBoxA 和 MessageBoxW,在 user32.inc 中有一句:

MessageBox equ <MessageBoxA>

MessageBoxA 和 MessageBoxW 对应两个字符集,一个是 ANSI,另一个为 UNICODE,每一个 ANSI 字符只占用一个字节宽,每一个 UNICODE 占两个字节宽,对于欧洲语言体系,ANSI 字符集就已经够了,但对于有成千上万个不同字符的几种东方语言体系来说,UNICODE 字符集更有用。

(本文完)

0 条回应

必须 注册 为本站用户, 登录 后才可以发表评论!