虽然16位的Windows程序已经近乎绝迹,但是有关复古程序以及研究加密狗的研究,往往会涉及这部分知识。
微软于1993年8月发布了最后一个16位的Windows系统,即Windows 3.11(同年发行的中文操作系统Windows 3.2也是16位操作系统)。在此之后问世的16/32位混合系统Windows 96/98/ME系统,以及32位的Windows NT系统都可以运行16位应用程序。后来推出的64位Windows NT系列操作系统不再支持16位应用程序。
16位应用程序的代码结构和MSDOS的程序十分相似。这种类型的可执行文件采用了一种名为“New Executable (NE)”的可执行程序格式。
本章的所有程序均由OpenWatcom 1.9编译。编译时的选项开关如下:
wcl.exe -i=C:/WATCOM/h/win/ -s -os -bt=windows -bcl=windows example.c
#include <windows.h>
int PASCAL WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
MessageBeep(MB_ICONEXCLAMATION);
return 0;
};
WinMain proc near
push bp
mov bp, sp
mov ax, 30h ; '0' ; MB_ICONEXCLAMATION constant
push ax
call MESSAGEBEEP
xor ax, ax ; return 0
pop bp
retn 0Ah
WinMain endp
这个程序不难分析。
#include <windows.h>
int PASCAL WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
MessageBox (NULL, "hello, world", "caption", MB_YESNOCANCEL);
return 0;
};
WinMain proc near
push bp
mov bp, sp
xor ax, ax ; NULL
push ax
push ds
mov ax, offset aHelloWorld ; 0x18. "hello, world"
push ax
push ds
mov ax, offset aCaption ; 0x10. "caption"
push ax
mov ax, 3 ; MB_YESNOCANCEL
push ax
call MESSAGEBOX
xor ax, ax ; return 0
pop bp
retn 0Ah
WinMain endp
dseg02:0010 aCaption db 'caption',0
dseg02:0018 aHelloWorld db 'hello, world',0
基于Pascal语言的调用约定要求:参数从左至右入栈(与cdecl相反)。调用方函数依次传递NULL、"hello, world"、"caption"和MB_YESNOCANCEL。这个规范还要求被调用函数恢复栈指针,所以RETN指令有一个0Ah参数,即被调用函数在退出的时候要释放10字节的栈空间。这种调用规范和stdcall(参阅64.2节)十分相似,只是参数传递的顺序是从左到右的“自然语言”顺序,
16位应用程序的指针是一对数据:函数首先传递的是数据段的地址,然后再传递段内的指针地址。本例子只用到了一个数据段,所以DS寄存器的值一直是可执行文件数据段的地址。
#include <windows.h>
int PASCAL WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
int result=MessageBox (NULL, "hello, world", "caption", MB_YESNOCANCEL);
if (result==IDCANCEL)
MessageBox (NULL, "you pressed cancel", "caption", MB_OK);
else if (result==IDYES)
MessageBox (NULL, "you pressed yes", "caption", MB_OK);
else if (result==IDNO)
MessageBox (NULL, "you pressed no", "caption", MB_OK);
return 0;
};
WinMain proc near
push bp
mov bp, sp
xor ax, ax ; NULL
push ax
push ds
mov ax, offset aHelloWorld ; "hello, world"
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
mov ax, 3 ; MB_YESNOCANCEL
push ax
call MESSAGEBOX
cmp ax, 2 ; IDCANCEL
jnz short loc_2F
xor ax, ax
push ax
push ds
mov ax, offset aYouPressedCanc ; "you pressed cancel"
jmp short loc_49
loc_2F:
cmp ax, 6 ; IDYES
jnz short loc_3D
xor ax, ax
push ax
push ds
mov ax, offset aYouPressedYes ; "you pressed yes"
jmp short loc_49
loc_3D:
cmp ax, 7 ; IDNO
jnz short loc_57
xor ax, ax
push ax
push ds
mov ax, offset aYouPressedNo ; "you pressed no"
loc_49:
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
xor ax, ax
push ax
call MESSAGEBOX
loc_57:
xor ax, ax
pop bp
retn 0Ah
WinMain endp
这段代码是基于前面那个例子进行了一些改动。
#include <windows.h>
int PASCAL func1 (int a, int b, int c)
{
return a*b+c;
};
long PASCAL func2 (long a, long b, long c)
{
return a*b+c;
};
long PASCAL func3 (long a, long b, long c, int d)
{
return a*b+c-d;
};
int PASCAL WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
func1 (123, 456, 789);
func2 (600000, 700000, 800000);
func3 (600000, 700000, 800000, 123);
return 0;
};
func1 proc near
c = word ptr 4
b = word ptr 6
a = word ptr 8
push bp
mov bp, sp
mov ax, [bp+a]
imul [bp+b]
add ax, [bp+c]
pop bp
retn 6
func1 endp
func2 proc near
arg_0 = word ptr 4
arg_2 = word ptr 6
arg_4 = word ptr 8
arg_6 = word ptr 0Ah
arg_8 = word ptr 0Ch
arg_A = word ptr 0Eh
push bp
mov bp, sp
mov ax, [bp+arg_8]
mov dx, [bp+arg_A]
mov bx, [bp+arg_4]
mov cx, [bp+arg_6]
call sub_B2 ; long 32-bit multiplication
add ax, [bp+arg_0]
adc dx, [bp+arg_2]
pop bp
retn 12
func2 endp
func3 proc near
arg_0 = word ptr 4
arg_2 = word ptr 6
arg_4 = word ptr 8
arg_6 = word ptr 0Ah
arg_8 = word ptr 0Ch
arg_A = word ptr 0Eh
arg_C = word ptr 10h
push bp
mov bp, sp
mov ax, [bp+arg_A]
mov dx, [bp+arg_C]
mov bx, [bp+arg_6]
mov cx, [bp+arg_8]
call sub_B2 ; long 32-bit multiplication
mov cx, [bp+arg_2]
add cx, ax
mov bx, [bp+arg_4]
adc bx, dx ; BX=high part, CX=low part
mov ax, [bp+arg_0]
cwd ; AX=low part d, DX=high part d
sub cx, ax
mov ax, cx
sbb bx, dx
mov dx, bx
pop bp
retn 14
func3 endp
WinMain proc near
push bp
mov bp, sp
mov ax, 123
push ax
mov ax, 456
push ax
mov ax, 789
push ax
call func1
mov ax, 9 ; high part of 600000
push ax
mov ax, 27C0h ; low part of 600000
push ax
mov ax, 0Ah ; high part of 700000
push ax
mov ax, 0AE60h ; low part of 700000
push ax
mov ax, 0Ch ; high part of 800000
push ax
mov ax, 3500h ; low part of 800000
push ax
call func2
mov ax, 9 ; high part of 600000
push ax
mov ax, 27C0h ; low part of 600000
push ax
mov ax, 0Ah ; high part of 700000
push ax
mov ax, 0AE60h ; low part of 700000
push ax
mov ax, 0Ch ; high part of 800000
push ax
mov ax, 3500h ; low part of 800000
push ax
mov ax, 7Bh ; 123
push ax
call func3
xor ax, ax ; return 0
pop bp
retn 0Ah
WinMain endp
当16位系统(MSDOS和Win16)传递long型32位“长”数据时(这种平台上的int型数据是16位数据),它会将32位数据拆成2个16位数据、成对传递。这种方法和第24章介绍的“32位系统处理64位数据”的方法十分相似。
此处的sub_B2是编译器开发人员编写的(仿真)库函数,用于长数据的乘法运算;即它可实现2个32位数据的乘法运算。程序中的其他库函数,请参见本书的附录D和附录E。
ADD/ADC指令分别对高低16位数据进行加法运算:ADD指令可设置/清除CF标识位,而ADC指令会代入这个标识位的值。同理,SUB/SBB指令对可实现32位数据的减法运算:SUB可设置/清除CF标识位,SBB会在计算过程中代入借位标识位的值。
在返回函数值的时候,32位的返回值通过DX:AX寄存器对回传。
另外,当主函数WinMain()函数向其他函数传递32位常量时,它也把常量拆分为1对16位数据了。
如果要把int型常数123当作long型32位参数,那么编译器就会使用CWD指令把AX里的16位数据符号扩展为32位的数据、再连同DX寄存器里的高16位数据一同传递。
#include <windows.h>
int PASCAL string_compare (char *s1, char *s2)
{
while (1)
{
if (*s1!=*s2)
return 0;
if (*s1==0 || *s2==0)
return 1; // end of string
s1++;
s2++;
};
};
int PASCAL string_compare_far (char far *s1, char far *s2)
{
while (1)
{
if (*s1!=*s2)
return 0;
if (*s1==0 || *s2==0)
return 1; // end of string
s1++;
s2++;
};
};
void PASCAL remove_digits (char *s)
{
while (*s)
{
if (*s>='0' && *s<='9')
*s='-';
s++;
};
};
char str[]="hello 1234 world";
int PASCAL WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
string_compare ("asd", "def");
string_compare_far ("asd", "def");
remove_digits (str);
MessageBox (NULL, str, "caption", MB_YESNOCANCEL);
return 0;
};
string_compare proc near
arg_0 = word ptr 4
arg_2 = word ptr 6
push bp
mov bp, sp
push si
mov si, [bp+arg_0]
mov bx, [bp+arg_2]
loc_12: ; CODE XREF: string_compare+21j
mov al, [bx]
cmp al, [si]
jz short loc_1C
xor ax, ax
jmp short loc_2B
loc_1C: ; CODE XREF: string_compare+Ej
test al, al
jz short loc_22
jnz short loc_27
loc_22: ; CODE XREF: string_compare+16j
mov ax, 1
jmp short loc_2B
loc_27: ; CODE XREF: string_compare+18j
inc bx
inc si
jmp short loc_12
loc_2B: ; CODE XREF: string_compare+12j
; string_compare+1Dj
pop si
pop bp
retn 4
string_compare endp
string_compare_far proc near ; CODE XREF: WinMain+18p
arg_0 = word ptr 4
arg_2 = word ptr 6
arg_4 = word ptr 8
arg_6 = word ptr 0Ah
push bp
mov bp, sp
push si
mov si, [bp+arg_0]
mov bx, [bp+arg_4]
loc_3A: ; CODE XREF: string_compare_far+35j
mov es, [bp+arg_6]
mov al, es:[bx]
mov es, [bp+arg_2]
cmp al, es:[si]
jz short loc_4C
xor ax, ax
jmp short loc_67
loc_4C: ; CODE XREF: string_compare_far+16j
mov es, [bp+arg_6]
cmp byte ptr es:[bx], 0
jz short loc_5E
mov es, [bp+arg_2]
cmp byte ptr es:[si], 0
jnz short loc_63
loc_5E: ; CODE XREF: string_compare_far+23j
mov ax, 1
jmp short loc_67
loc_63: ; CODE XREF: string_compare_far+2Cj
inc bx
inc si
jmp short loc_3A
loc_67: ; CODE XREF: string_compare_far+1Aj
; string_compare_far+31j
pop si
pop bp
retn 8
string_compare_far endp
remove_digits proc near ; CODE XREF: WinMain+1Fp
arg_0 = word ptr 4
push bp
mov bp, sp
mov bx, [bp+arg_0]
loc_72: ; CODE XREF: remove_digits+18j
mov al, [bx]
test al, al
jz short loc_86
cmp al, 30h ; '0'
jb short loc_83
cmp al, 39h ; '9'
ja short loc_83
mov byte ptr [bx], 2Dh ; '-'
loc_83: ; CODE XREF: remove_digits+Ej
; remove_digits+12j
inc bx
jmp short loc_72
loc_86: ; CODE XREF: remove_digits+Aj
pop bp
retn 2
remove_digits endp
WinMain proc near ; CODE XREF: start+EDp
push bp
mov bp, sp
mov ax, offset aAsd ; "asd"
push ax
mov ax, offset aDef ; "def"
push ax
call string_compare
push ds
mov ax, offset aAsd ; "asd"
push ax
push ds
mov ax, offset aDef ; "def"
push ax
call string_compare_far
mov ax, offset aHello1234World ; "hello 1234 world"
push ax
call remove_digits
xor ax, ax
push ax
push ds
mov ax, offset aHello1234World ; "hello 1234 world"
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
mov ax, 3 ; MB_YESNOCANCEL
push ax
call MESSAGEBOX
xor ax, ax
pop bp
retn 0Ah
WinMain endp
这个程序使用了“near”和“far”两种不同类型的指针。每种指针都对应着16位 8086CPU的一种特定的指针寻址模式。这方面详细内容,请参见本书第94章的详细介绍。
“near”指针的寻址空间是当前数据段(DS)内的所有地址。字符串比较函数string_compare() 读取2个指针,把DS寄存器的值当作寻址所需的基(段)地址、对这两个指针进行寻址。所以此处的“mov al,[bx]”指令等效于“mov al, ds:[bx]”指令,只不过原指令没有明确标出它使用的DS寄存器而已。
“far”指针的寻址空间不限于当前数据段,它可以是其他DS段的内存地址。由于需要指定基(段)地址,所以2个16位数据才能表示1个far型指针。本例的 string_compare_far()函数从2对16位数据里提取2个内存地址。函数把指针的基地址存入段寄存器ES,然后在使用Far指针寻址时通过基地址寻址(mov al, es:[bx])。本章的例2表明,16位程序的MessageBox()函数(属于系统函数)使用的也是far指针。确实,当Windows内核访问文本字符串指针时,它不了解字符串指针的基地址是什么,所以在调用内核函数的时候需要指明指针的段地址。
near指针的寻址范围是64k,这恰好是1个数据段的长度。对于小型程序来说,这种指针可能就够用了、不必在每次寻址的时候都要传递指针的段地址。大型的程序通常会占用多个64k的数据段,所以就要在每次寻址的时候指明指针的数据段(段地址)。
代码段也有寻址意义上的差别。一个64k内存段就可以盛下所有指令的小型程序,可以只用CALL NEAR指令调用其他函数;其被调用方函数可以只用RETN指令返回调用方函数。但是,大型程序会占用数个代码段,它就需要使用CALL FAR指令、用1对16位数据作跳转的目的地址;而去其被调用方函数就必须通过RETF指令返回调用方函数。
这就是编译器“内存模型(memory model)”选项的实际意义。
面向MS-DOS和Win16的编译器,为各种内存模型准备了相应的不同库。这些库文件在代码指针和数据指针的寻址模式存在相应的区别。
#include <windows.h>
#include <time.h>
#include <stdio.h>
char strbuf[256];
int PASCAL WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
struct tm *t;
time_t unix_time;
unix_time=time(NULL);
t=localtime (&unix_time);
sprintf (strbuf, "%04d-%02d-%02d %02d:%02d:%02d", t->tm_year+1900, t->tm_mon, t-> tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
MessageBox (NULL, strbuf, "caption", MB_OK);
return 0;
};
WinMain proc near
var_4 = word ptr -4
var_2 = word ptr –2
push bp
mov bp, sp
push ax
push ax
xor ax, ax
call time_
mov [bp+var_4], ax ; low part of UNIX time
mov [bp+var_2], dx ; high part of UNIX time
lea ax, [bp+var_4] ; take a pointer of high part
call localtime_
mov bx, ax ; t
push word ptr [bx] ; second
push word ptr [bx+2] ; minute
push word ptr [bx+4] ; hour
push word ptr [bx+6] ; day
push word ptr [bx+8] ; month
mov ax, [bx+0Ah] ; year
add ax, 1900
push ax
mov ax, offset a04d02d02d02d02 ; "%04d-%02d-%02d %02d:%02d:%02d"
push ax
mov ax, offset strbuf
push ax
call sprintf_
add sp, 10h
xor ax, ax ; NULL
push ax
push ds
mov ax, offset strbuf
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
xor ax, ax ; MB_OK
push ax
call MESSAGEBOX
xor ax, ax
mov sp, bp
pop bp
retn 0Ah
WinMain endp
“unix_time”是个32位数据。它首先被Time()函数存储在寄存器对DX:AX里,而后被主函数复制到了2个本地的16位变量。接着这个指针(地址对)又被传递给localtime()函数。Localtime()函数把这个指针指向的数据解析为标准库定义的tm结构体,返回值是这种结构体的指针。另外,这也意味着如果不使用完其返回的数值,就不应重复调用这个函数。
在调用time()函数和localtime()函数的时候,编译器使用的是Watcom调用约定:前4个参数分别通过AX、DX、BX和CX寄存器传递,其余参数通过数据栈传递。遵循这种调用约定的库函数,其函数名称(汇编层面)的尾部也有下划线标识。
不过,sprintf()函数遵循的调用约定既不是PASCAL也不是Watcom,所以编译器使用常规的cdecl规范传递参数(请参考64.1节)。
我们对刚才的例子略作改动,使用全局变量再次实现它的功能:
#include <windows.h>
#include <time.h>
#include <stdio.h>
char strbuf[256];
struct tm *t;
time_t unix_time;
int PASCAL WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
unix_time=time(NULL);
t=localtime (&unix_time);
sprintf (strbuf, "%04d-%02d-%02d %02d:%02d:%02d", t->tm_year+1900, t->tm_mon, t-> tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
MessageBox (NULL, strbuf, "caption", MB_OK);
return 0;
};
unix_time_low dw 0
unix_time_high dw 0
t dw 0
WinMain proc near
push bp
mov bp, sp
xor ax, ax
call time_
mov unix_time_low, ax
mov unix_time_high, dx
mov ax, offset unix_time_low
call localtime_
mov bx, ax
mov t, ax ; will not be used in future...
push word ptr [bx] ; seconds
push word ptr [bx+2] ; minutes
push word ptr [bx+4] ; hour
push word ptr [bx+6] ; day
push word ptr [bx+8] ; month
mov ax, [bx+0Ah] ; year
add ax, 1900
push ax
mov ax, offset a04d02d02d02d02 ; "%04d-%02d-%02d %02d:%02d:%02d"
push ax
mov ax, offset strbuf
push ax
call sprintf_
add sp, 10h
xor ax, ax ; NULL
push ax
push ds
mov ax, offset strbuf
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
xor ax, ax ; MB_OK
push ax
call MESSAGEBOX
xor ax, ax ; return 0
pop bp
retn 0Ah
WinMain endp
虽然编译器保留了汇编宏t的赋值指令,但是这个值实际上没有被后续代码调用。因为编译器无法判断其他模块(文件)是否会访问这个值,所有保留了有关的赋值语句。