第53章 16位的Windows程序

虽然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

53.1 例子#1

#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

这个程序不难分析。

53.2 例子#2

#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寄存器的值一直是可执行文件数据段的地址。

53.3 例子#3

#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

这段代码是基于前面那个例子进行了一些改动。

53.4 例子#4

#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位数据一同传递。

53.5 例子#5

#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的编译器,为各种内存模型准备了相应的不同库。这些库文件在代码指针和数据指针的寻址模式存在相应的区别。

53.6 例子#6

#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节)。

53.6.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的赋值指令,但是这个值实际上没有被后续代码调用。因为编译器无法判断其他模块(文件)是否会访问这个值,所有保留了有关的赋值语句。