当计算机技术越来越先进,越来越快速更新,作为电子FANS,发现这样的现状不仅带给从事这个行业的人更多的便利,还有更多的迷失!而DOS就像是那个本应有活力的游乐园,收集资料的过程中发现,国外确实把DOS当成了计算机世界的大游乐场!国内很多在玩开发板的的同学几乎都不懂得,其实个人电脑才是功能最齐全的“开花板”! - by Jimbowhy
计算机的快速发展可以让人不能在新的操作系统上运行旧的开发程序,但却又可以让人通过虚拟机器来运行旧的操作系统,这种事情太矛盾了,和人格分裂没什么两样。在写作的时候,怕是绝大部分机器都在使用 Windows xp 和 Windows 7 了吧,不过,至少我,还是DOS、Windows 3x/9x 还有最新的 Windows 7 上来回转,偶尔还用一下 Linux。为了可以在我的 ,不对,是微软的Windows 7上运行DOS程序,或开发程序,需要以下任意一种工具,从使用的简易度排列:
即使 Windows 95 是黑白的,我也会喜欢的。
有了以上的工具,后就可以开始安装操作系统了。除DOSBOX仅支持 DOS 和 Windows 3.x 外,其它工具基本上支持各种操作系统的安装。具体安装过程序就不展开了,对于 DOSBOX,配置命令比较容易掌握,相当于使用DOS系统,用它来安装 Windows 3.x 就像在DOS安装其它程序一样。使用内置的 Mount 来映射安装目录来一个当作C盘的目录即可以运行 Windows 3.x 安装程序。DOSBOX 也可以安装Windows 95,借用Bochs的磁盘工具bximage来创建用于安装Windows95的系统盘,再用这个盘来启动就可以了。D-Fend是一个DOSBOX配置的图形界面工具,如果不熟悉DOSBOX则可以使用它来帮助配置。当然不用配置也可以直接运行 DOSBOX,像以下这条命令就可以开始无调试窗口运行,并且将 watcom 目录加载为 C 盘,同时还设置了 CPU 的模拟速度:
DOSBox.exe -noconsole -c "mount c c:\watcom" -c "config -set 'cpu cycles=10000'"
DOSBOX安装 Windows95B OSR2.1 步骤参考,安装光盘已经上传。注意imgmount命令执行方式选择很重要,不正确的参数可能导致磁盘不被识别。本机使用官方的0.74版本,系统为 Windows 7 64位。DosBox-X是不错的版本,在GitHub上有下载,还有DosBox Svn Daum 版。借用Bochs的磁盘工具bximage创建一个160MB的flat模式硬盘映像c.img,得到参数CHS=325/16/63,扇区大小使用默认的512字节,将这些参数用来设备DOSBOX的映像装载命令,如下如果第一条命令装入磁盘无法识别,可以替换第二条试试。然后,通过Windows 98引导盘来分区、格式化,最后安装系统到映像上,最后修改DOSBOX为硬盘启动。期间可能需要用到Bootice这个工具来修改引导区的信息。安装完后,进行系统可能会花屏,没有系统更新一下默认的S3显卡驱动就好了,下载地址在后面使用S3 Trio 32/64 PCI驱动。不更新驱动,直接使用16色模式也不会花屏。如果使用 DOSBox Daum,还可以加载显卡BIOS,S3 Trio 64 BIOS。
# 1. load image into dosbox
IMGMOUNT 2 .\bochs\c.img -t hdd -fs none -size 512,63,16,325
IMGMOUNT c .\bochs\c.img -t hdd
# 2. load floppy into dosbox and boot it
IMGMOUNT a .\bochs\win98.img -t floppy
boot .\vpc\win98.img -l a
# 3. use fdisk to create primary partition & format it
fdisk
format /q /v:win95 c:
# 4. mount windows cdrom or image & setup & boot windows
# SMARTDRV.EXE & #13895-OEM-0000716-68627 may be useful
IMGMOUNT D .\win95b_osr2.1.iso -t iso
boot -l c
提示,可以用winimage工具来管理img磁盘映射,也可以用Windows系统自带的diskpart磁盘工具来加载img磁盘映射,通过计算机管理中的磁盘页面操作,或都通过命令行来实现,以下是两个脚本文件分别用来加载和卸载,保存为txt文件就可以了,使用 diskpart /s 命令来执行脚本:
REM diskpart /s mount.txt
select vdisk file="c:\win3x\vpc\d3.vhd"
attach vdisk
REM diskpart /s unload.txt
select vdisk file="c:\win3x\vpc\d3.vhd"
detach vdisk
当前系统就选择了DOSBOX来运行 MSDOS 和 Windows 3.x,有了运行环境,接下来就是选择开发环境。开发工具就更加丰富了,PC世界不就是从前发展到现在的吗,前人有的东西基本都还在,没有随风飘逝。可选择就列表吧:
- Microsoft(R) C/C++, Version 7.0 - Borland(R) Delphi 1.02
- Microsoft(R) QuickC Version 2.50 - Borland(R) Pascal 7.0
- Microsoft(R) QuickC For Windows 1.0 - Borland(R) Turbo Assembler 2.0
- Microsoft(R) C Professional 6.0 - Borland(R) C++ 3.1
- Microsoft(R) Quick Basic 4.5 - Borland(R) Turbo C 2.01
- Microsoft(R) BASIC Professional 7.1 - Borland(R) Turbo C++ 3.0
- Microsoft(R) Macro Assembler 6.11 - Borland(R) Turbo C++ 3.1 For Win
- Microsoft(R) Visual Basic 1.0 for DOS - DJGPP - GCC for DOS by DJ Delorie
- Microsoft(R) Visual C++ 1.52c for Win3x - Open Watcom C/C++/FORTRAN Version 1.3
- Watcom C/C++ version 11.0c Update - Watcom CPP 10.0a 1994_win3x_dos ISO
- Watcom C C++ 11.0b ISO - The Netwide Assembler
- GNU C/C++ Compiler for DOS and Desqview X - IBM Macro Assembler 1.0
- IBM Personal Computer BASIC Compiler - IBM C Compiler for IBM PC/XT/AT computers
- The High Level Assembler - GNU Assembler - GAS not for DOS
当前系统选择使用MASM,NASM等等汇编语言,还必选的 Watcom,有几个理由,它开源,它多平台,可以直接在Win32环境下运行,也可以在DOS/Win16环境下运行,它支持多语言,性能还不差。最后,它还支持 DOS 16/32-bit,WIN 16/32-bit,Linux ELF,还有其它的程序格式,在DOS32的类型上还有DOS/4GW,大内存肯定是没问题了。Watcom的历史可以追溯到1965年加拿大的学生Waterloo的团队开发了叫WATFOR的Fortran编译器,运行在IBM 7040平台,基于WSL语言。20世纪80年代后期用C重写,开始支持C,1988年推出PC版 WATCOM C++ 6.0 能够产生优于其他编译器的代码,可由于多个平台 如 DOS,Windows,OS/2 ,Windows NT。很多游戏 包括 DOOM,Descent,Duke Nukem 3D 都有它的影子,最熟悉的莫过于DOS/4GW这个工具了。
安装 Watcom 后,可以阅读 readme.txt,里面有如何配置环境变量的指导,如果使用Windows NT系列系统并且安装目录为 C:\WATCOM,则可以将以下内容保存为bat脚本,双击它就可以使用 Watcom 的各种工具了:
@echo off
SET WATCOM=C:\WATCOM
SET PATH=%WATCOM%\BINNT;%PATH%
SET EDPATH=%WATCOM%\EDDAT
SET INCLUDE=%WATCOM%\H;%WATCOM%\H\NT
SET FINCLUDE=%WATCOM%\SRC\FORTRAN
cmd /k
汇编选择了开源的NASM,它支持众多程序格式,同时也支持64位编程,而且语言比MASM简洁不繁俗,我认为一款好的x86汇编程序应该可以在DOS、Win32平台运行,而且可以按指定的目标平台输出编译程序。
试想这些软件热卖热卖的时候,很多人想要都得不到的吧。到现在这么旧的“破烂”,估计M$都懒得收费了吧。现在这一切都是免费的,真要把我高兴坏了。最后可以选择一个编辑器作为代码编辑工具,我选择使用 editplus,它小巧,功能齐全,支持正则,可以设置编译工具协同工作。手上还要有一款静态反汇编工具来查看程序文件。像 w32dasm 这种功能太简单,像 c32asm 这就是玩界面的货。像ida又太宏大,对于新手可能难以操控,但无论如何它是我见过最智能的反解软件,它具有图形化代码流程和详尽的系统功能注解,而且各种程序格式都可以应付,交叉平台操作,Windows平台下可以反解Linux程序,也可以其它的CPU构架如ARM。IDA的最大特色就是交互,正如其名 Interactive DisAssembly,同时它又是调试器。对于一般用途,甚至是深入的破解免费版的5.0就足已,可谓神器,OllyDBG之众与之相比,简直就是小儿科。如果想简单点,还是用 ETU-Dasm 吧,简单,因为主要用于自己的代码调试,没有涉及复杂的反解代码,所以没有必要使用太复杂的动态反编译功能,而且像 OllyDBG 这样的动态反汇编也不支持DOS程序。如果选择开源的NASM,它也有一个反汇编工具 ndisasm.exe 支持16/32/64−bit。如果你是新手,那么按以上提供的软件列表下载安装吧,开始新奇的体验吧!
1981年,微软发现了第一个汇编语言版本 Macro Assembler,中文称为宏汇编程序。在那个年代,CPU还是8086,一切都正在开始,硬件不算太复杂,指令也还不算多,程序也比较小,这个小是用kb为较量的小,所以1MB的寻址几乎就真的像当时假想的那样,足够了。早期的宏汇编程序的命令文件是 MASM.EXE,直到1991年,MASM 6.0 的发布,命令改名为 ML.EXE,而原有的命令文件 MASM.EXE 则是一个兼容驱动程序。
1999,微软发布了 MASM 6.14 补丁,ML.EXE 可作为32位控制台程序运行。这个版本中增加了对 Pentium III 的 SIMD 指令集的支持。2000年,随补丁包 Visual C++ 6.0 Processor Pack 发布的是 Macro Assembler Version 6.15.8803。现在最新的宏汇编和Visual Studio一起发行,支持更多的CPU功能特性。MASM作为微软平台上重要的开发工具之一,国外的MASM爱好者自行整理出了一个MASM32,用来在Win32环境下开发窗口程序。
在Win98ddk.exe中的 BINS_DDK.CAB 压缩包还可以找到完整的 MASM 6.11d 版本,但它提供的是增量链接程序 5.12.8181,不能用来链接16位程序。
用Microsoft的产品编写Win32程序,不管是使用VC还是MASM,都必须使用COFF (Microsoft Common Object File Format) 格式,32位的Link.exe只支持将COFF格式的obj文件链接成PE文件,另外所有的导入库等支持文件的格式也全部是COFF格式的。另外MASM汇编和C语言不同,为了使用汇编来写Win32程序,就需要一份MASM可以使用的头文件,前辈们都已经做了,MASM32安装包就有。要编译16位程序的话,就要使用 OMF (Microsoft Object Module Format) 文件格式,这种文件可以由旧式的链接程序 LINK16.EXE 支持,如MASM 6.11中自带的链接命令 link.exe,这个链接命令是段模型程序链接器 Segmented Executable Linker Version 5.31.09,比较好用的版本是1994年的5.60.339,可以在 Visual C++ 1.50中找到,也可以上MASM32官网上直接下载安装包,或者在Iczelion网站上下载都行。而 Visual Studio 97 提供的链接程序则是32位的增量链接程序 Incremental Linker Version 5.10.7303,使用COFF对象文件。
ML.EXE编译程序后会生成对象文件,它默认会调用 link.exe 命令来完成后面的链接工作,如果不想通过默认的方式生成程序,就可以指定 /c 选项只让 ML.exe 完成前期编译的工作,然后另外使用链接命令来生成程序。使用链接命令时,可以通过 /subsystem 选项来生成程序类型,如窗口程序 windows,控制台程序 console。链接DLL动态链接库时就要指定 /DLL 选项,同时还可能需要使用 /def 指定列表定义文件,或用 /base 来指定库文件加载时的默认装载地址。如果需要指定DOS兼容性代码块则可以通过选项 /stub 来指定MZ程序模块。这是一个有趣的的选项,PE文件有个简单的DOS文件头,在DOS下执行时会打印提示信息告诉用户程序要运行在Windows系统上,这部分功能称为DOS STUB,可以用完整的DOS程序来替换它,这样编译后的Win32程序就可以在DOS下使用。
ML [ /options ] filelist [ /link linkoptions ]
/AT Enable tiny model (.COM file) /nologo Suppress copyright message
/Bl Use alternate linker /Sa Maximize source listing
/c Assemble without linking /Sc Generate timings in listing
/Cp Preserve case of user identifiers /Sf Generate first pass listing
/Cu Map all identifiers to upper case /Sl Set line width
/Cx Preserve case in publics, externs /Sn Suppress symbol-table listing
/coff generate COFF format object file /Sp Set page length
/D[=text] Define text macro /Ss Set subtitle
/EP Output preprocessed listing to stdout /St Set title
/F Set stack size (bytes) /Sx List false conditionals
/Fe Name executable /Ta Assemble non-.ASM file
/Fl[file] Generate listing /w Same as /W0 /WX
/Fm[file] Generate map /WX Treat warnings as errors
/Fo Name object file /W Set warning level
/FPi Generate 80x87 emulator encoding /X Ignore INCLUDE environment path
/Fr[file] Generate limited browser info /Zd Add line number debug info
/FR[file] Generate full browser info /Zf Make all symbols public
/G Use Pascal, C, or Stdcall calls /Zi Add symbolic debug info
/H Set max external name length /Zm Enable MASM 5.10 compatibility
/I Add include path /Zp[n] Set structure alignment
/link /Zs Perform syntax check only
在使用MASM的伪指令时,还需要注意点,比如.startup这个,使用它时,在它之前的代码会统统无效,等于没写。还有 ORG 伪指令,这个家伙也会扰乱内存布局的,特别是在COM程序上,使用这个东西要多留意测试,比如下面这个COM程序骨架:
.MODEL tiny
;------------------------------------------------------------------
.STACK 256
;------------------------------------------------------------------
.DATA
;------------------------------------------------------------------
msg db 'Hey you!', 0, '$'
.CODE
;------------------------------------------------------------------
.startup
jmp main
org 100h
_print proc ; @txt string to print
; INT 21h subfunction 9 requires '$' to terminate string
push bp
mov bp, sp
mov ah, 9
mov dx, [bp+04h]
int 21h
pop bp
ret
_print endp
main proc
mov BYTE PTR[bx + 81h], '$'
lea ax, msg
push ax
call _print
inc sp ; add sp, 2
exit: ; int 21 with 4ch
mov ax, 4C00h
int 21h
main endp
end
指定开始点使用了 .startup 伪指令设置了程序入口,然后使用JMP指令跳转到main过程执行,这点逻辑上是完全没问题的。但是后面这条 org 伪指令就很有问题了,因为它会覆盖掉.startup指令设置的入口指令,导致编译后的程序代码不符合逻辑。如果直接注解 .startup 伪指令,编译后结果会更奇怪,它会生成以下这样的程序指令:
00000000 E9 09 01 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
........ Duplications
00000100 55 8B EC B4 09 8B 56 04 CD 21 5D C3 C6 87 81 00 U.....V..!].....
00000110 24 8D 06 20 01 50 E8 E7 FF 44 B8 00 4C CD 21 00 $.. .P...D..L.!.
00000120 48 65 79 20 79 6F 75 21 00 24 Hey you!.$
反汇编如下:
SS:0100 E90901 JMP 020C
...
SS:020C C687810024 MOV BYTE PTR [BX+81H], 24 ; nothing much useful
SS:0211 8D062001 LEA AX, WORD PTR [120H]
...
COM程序装入时,文件的第一个字节会装入到当前段的100H偏移处,并且由这里开始执行,即第一条指令就是跳转指令,注意文件中JMP指令的操作数是 0109,即相对JMP指令后面的109字节位置,因为这条JMP指令本身占3个字节,代码在执行时,实际跳转地址就是100+109+3=020C。这里有一条MOV指令,占5个字节,纯属无意义指令,透明处理它。接下来这条指令是要将输出字符串的地址取并压栈当函数参数传递。本来这个过程可以直接用一条PUSH指令将字符串变量的地址当立即数直接压栈的,可惜8086的指令只支持寄存器和内存地址,而不支持立即数。注意LEA指令的操作数是120H,如果这个地址是在COM程序的文件中,偏移值是对的,这个偏移刚好是字符串的所在,但是COM加载到内存执行时以100H偏移为入口,并且COM程序是没有地址重定位功能的,后面DOS程序小节涉及重定位内容,这一切最终就产生了100H的偏差,程序运行结果就出错了。
为了混用汇编和C语言进行编程,就需要配套使用微软的C/C++语言编译器,也即是 Visual C++,它的前身是 Microsoft C/C++,最高版本好像是1992年的Microsoft(R) C/C++, Version 7.0,我手上就有MSC70.zip,还有1991年发布的MSC60.zip两个版本。更早的C语言开发工具就是1990年的QuickC(R) Compiler 2.51 Professional。然后就是Visual系列的天下了,最早的一版是1992年发行的Visual C++ 1.0,20张3.5英寸磁盘装,现在能看到的只有磁盘镜像了,镜像中的Link.EX_是SZDD压缩格式。这个版本支持16位和32位开发,集成MFC框架,集成资源编辑器。链接程序为32位 Executable Linker 1.00,同时提供 link.exe 和 link32.exe 两个命令。在 Win PC World 上下载到一个称为 msvc10_32s.iso 的镜像,它包含了 msvc32s 和 msvcnt 两套开发工具,标明是 Visual C++ 1.0,但感觉不是。因为最后一个DOS开发版本是1993发行的 Visual C++ 1.52c,就是这一套工具开始,链接程序不再叫做 Executable Linker,而是叫做分段模式程序链接器 Segmented Executable Linker,随CD发行的链接程序版本为 5.60.339,这个链接程序也是MASM32中使用的16位程序链接器,C/C++编译程序为 Optimizing Compiler 8.00c,光盘镜像已经上传,可以在后面的链接找到。直到今天,它仍然具有强大的生命力,它附带的MFC可以用来开发MS-DOS程序,一些为MSDOS、Windows 3.1编写16位应用程序的程序员还在使用这个版本,它也提供了DOS开发扩展,由 Phar Lap 这家公司提供的技术,这家公司也是VCPI标准 Virtual Control Program Interface 的制订者,同时也是DPMI制订者之一。我个人也很喜欢这样小巧的开发工具,因为我只需库文件和编译工具而已,完成编码后剩下的工作就交给make工具而不是IDE。另外通过WIKI发现是 Visual C++ 1.0 32-bit Edition,也就是说1.0版本有两套发行版本用于16位和32位程序开发。下载光盘镜像后,可以通过DOSBOX运行Windows 3.1来安装,DOSBOX的ISO加载命令如下:
IMGMOUNT d "F:\Forgetful\Visual\Visual.C++.1.52c.iso" -t cdrom -fs iso
使用 Visual C++ 的链接器时,注意库文件和头文件的引用路径,link.exe程序是通过环境变量的 INCLUDE 和 LIB 来定位的,虽然可以通过 /I 参数来添加头文件目录,却不能指定库文件的搜索目录,一个解决方法是使用完全路径来指定文件地址,或者先设置环境变量LIB。如果找不到库文件就会提示L4051,错误代码信息可以在帮助文件ERRORS.HLP中找到。可以通过 /p 选项来打印当前NMAKE系统的设置信息,包括宏定义,推测规则 Inference Rules。另外在这个版本中,RC.EXE是负责链接资源和程序的,并不像现在使用的新版本link.exe来负责链接资源。而且,它还是以前那种使用指示脚本的执行方式,在我查完所有可以查的资源后,并没有发现 NMAKE 可以像 WMAKE 一样支持 .BEFORE, .AFTER 这一类点指令来指定预备动作命令和后备动作命令。所以还是用BAT脚本设置环境变量吧,至少比起为每个目标都设置一次环境变量要好得多,不然就使用WATCOM提供的WMAKE工具:
SET PATH=C:\Win3x\MSVC152\BIN;
SET INCLUDE=C:\Win3x\MSVC152\INCLUDE;C:\Win3x\MSVC152\MFC\INCLUDE
SET LIB=C:\Win3x\MSVC152\LIB;C:\Win3x\MSVC152\MFC\LIB
Visual C++ 1.52C的link.exe命令选项参数格式如下,注意命令中没有提示很重要的一个功能,在命令后面添加一个分号可以让链接器使用默认参数;使用 /NOL 参数可以禁止指针LOGO信息;通过 /CO 参数可以生成调式信息;通过CL.EXE命令来间接调用它也可以简化使用它的繁复:
LINK @
LINK ,,,,
Output Option Comment
/ALIGN:n Align segments at boundaries of n bytes
/CO Generate Debugging Information. Use with /Zi compiler option
/MAP Create MAP File. Generate list of segments and public symbols
/ONERROR:NOEXE Do not write executable file to disk when a linker error occurs.
/INFO Produce More Detailed Output. Display link status information.
/TINY Produce tiny memory module executable file, produce COM File.
/ALIGNment /HIGH /NOIgnorecase /PACKFunctions
/BATCH /INFOrmation /NOLOGO /PAUSE
/COdeview /LIB:"libraryname" /NONULLSdosseg /PCODE
/CPARMaxalloc /LINEnumbers /NOPACKCode /PMtype
/DOSSEG /MAP /NOPACKFunctions /Quicklibrary
/DSALLOCate /NODefaultlibrarysearch /OLDOVERLAY /SEGments
/DYNAMIC /NOExtdictionary /ONERROR:NOEXE /STACK
/EXEPACK /NOFARCALLtranslation /OVerlayinterrupt /TINY
/FARCALLtranslation /NOFREEMEM /PACKCode /Warnfixup
/HELP /NOGROUPassociation /PACKData
另外,link.exe 命令的使用格式真可谓是筋骨奇特,是个百年难得一遇的链接器,我能有幸遇上而且练好它,总算是可以增加几成功力,以下是编译COM程序的示例:
cl /c /AT hello.c
link /tiny /noe crtcom.lib+hello,hello.com,,;
如果你也想练好这门绝学,我告诉你怎么找到秘籍,首先要翻山越岭到达后面通往下载Visual C++ 1.52C的链接处,用DOSBOX安装的Windows 3.2安装好它,在HELP目录下就有一本叫做MSCOPTS.HLP的秘籍,里面的内容绝对是兵家必争之绝学。包含三大招式,Compiler Options,Linker Options,Module-Definition File Statements。然而还有更加齐全的DOS编程资源,那就是微软的官方资源,我称之为MSDN4DOS的宝贝,见后面一个小节。
结合上面汇编程序的例子,这里再添加一个C语言程序用来调用汇编程序的print函数,注意C语言的函数改名规则是加下划线前缀,所以声明print()函数在编译后其实是调用_print()函数,另外汇编代码中也不能使用tiny内存模型了,它会导致链接器按COM程序来处理,结果是两个程序入口冲突,只有汇编这个才有效。另外,尝试将两种语言的代码编译成一个COM程序,但是字符串的地址却无法正确引用到,偏移了100h:
void print(char*);
int main(int c, int *args)
{
print("This is test.c end.$");
return 0;
}
然后就是NMAKE脚本的编写了,有了脚本,只需要执行 nmake test.ext 就可以完成程序的自动编译工作,所以没有IDE工作更轻松,添加DEBUG=1参数就可以增加CodeView的调试信息:
#-----------------------------------------------------------------#
# Microsoft NMake makefile demo by Jimbowhy 2016/3/31 13:02:51 #
# Usage: #
# nmake test.exe #
#-----------------------------------------------------------------#
CFLAGS =/nologo /YX /AS /D "_DOS" /G2 /Zp1 /W3 /FR /GA
LFLAGS =/NOLOGO /NOI /NOD /STACK:5120 /ONERROR:NOEXE
LIBS =oldnames.lib
!if "$(DEBUG)" == "1"
CFLAGS = $(CFLAGS) /Od /D "_DEBUG" /Zi
LFLAGS = $(LFLAGS) /CO
!else
CFLAGS = $(CFLAGS) /Gs /Ox /D "NDEBUG"
!endif
PROJET = TEST
all: $(PROJET).EXE
OBJS: $(PROJET).C
ml /c tsr.asm
$(CC) $(CFLAGS) /c $(PROJET).C
$(PROJET).EXE:: OBJS
link $(LFLAGS) $(PROJET).OBJ TSR.OBJ $(LIBS);
clean:
@if exist *.PCH del *.PCH
@if exist *.OBJ del *.OBJ
@if exist *.SBR del *.SBR
@if exist *.EXE del *.EXE
编译命令可以使用 /AT, /AS, /AM, /AC, /AL, /AH 来指定 Tiny, Small, Medium, Compact, Large, Huge 五种内存模型;使用 /YX 打开预编译头 PCH 文件功能;使用 /Fa, /FI, /Fc 选项来生成含有源代码、汇编、机器指令的列表文件。如果使用到了VC的堆栈检查功能而又沒有包含库文件,就会收到这样的错误提示。默认的编译选项是 /Ge 激活f堆栈检查功能的,可以用 /Gs 来关闭它,或者使用默认库文件,即把 /NOD 选项去掉,而且 NOD 一般和 oldnames.lib 库配合使用。
TEST.OBJ(test.c) : error L2029: '__acrtused' : unresolved external
TEST.OBJ(test.c) : error L2029: '__aNchkstk' : unresolved external
在早期的Microsoft C Compiler 5.1在编译 MS-DOS 程序时,如果代码中含有入口函数 main()、WinMain()、LibMain(),就会在OMF中引用了一个全局变量 _acrtused,它可以使链接程序引入C语言的初始代码,为了禁止它可以使用这条语句:
int _acrtused = 0;
在32-bit程序中是没有使用这个功能的,根据不同的程序类型,入口函数如下:
Executable Type Entry Point Entry Point Name
----------------------------------------------------
Console main() _mainCRTStartup
Windows WinMain() _WinMainCRTStartup
DLL DllMain() _DllMainCRTStartup@12
在DOS环境下,微软提供的最好调试工具就是CodeView了,功能也很强大,CodeView的使用手册在后面有链接。
对于更加齐全DOS编程资源,那就是微软的 Microsoft Programmer’s Library,我称之为MSDN4DOS,手头上的1.3版CDROM已经上传,最新的版本是1.4,可惜下载不到。光盘含有11个参考目录,其中就有Duncan的Advanced MS-DOS Programming,还有 the MS-DOS Encyclopedia,1986/1987年出版的 Intel 80286/80386 Programming Reference Manual,MASM References等等自然是囊中物,文件的编码是 United States DOS 437,如果以中文编码打开就会看到很多乱七八糟的符号,但其实正确的编码是这样的:
About Microsoft Programmer's Library!
╔══════════════════════════════════════════════════════════╗
║ MICROSOFT PROGRAMMER'S LIBRARY ║
║ CD-ROM Database of PC Programming References ║
║ Copyright 1988, 1989 Microsoft (c) All Rights Reserved ║
╚══════════════════════════════════════════════════════════╝
Proficient C. Copyright (c) 1987 by Augie Hansen.
Advanced MS-DOS. Copyright (c) 1986 by Ray Duncan.
The MS-DOS Encyclopedia. Copyright (c) 1988 by Microsoft Press.
Inside OS/2. Copyright (c) 1988 by Microsoft Press.
Programming Windows. Copyright (c) 1988 by Charles Petzold.
Programmer's Guide to PC and PS/2 Video Systems.
Copyright (c) 1987 by Richard Wilton.
Microsoft QuickC Programming.
Copyright (c) 1988 by The Waite Group, Inc.
Microsoft QuickBASIC Programmer's Toolbox.
Copyright (c) 1988 by John Clark Craig.
Intel 386/387/286/287 Programmer's Reference.
Copyright (c) 1987 by Intel Corporation.
MSDN从DOS时代一直发现到WEB时代,原始的版本Microsoft Advisor Help,在Microsoft Programmer’s
Workbench 提供了 QuickHelp 程序。MSL -> WinHelp -> HTML Help -> Help Viwer,一直在进化中,而 HTML Help 是我觉得最开放和使用最方便的版本。在MSL的CDROM中,MSLIB目录只包含主程序 MSL.EXE 和资料目录文件,主要是RPC和RES文件,后者是显示的目录信息,前者是与资料文件关联的信息。以硬件编程的资料为例,目录文件HW.PRC就关联了光盘下BOOKS子目录的资料文件,如视频系统编程 Programmer’s Guide to PC & PS/2 Video Systems 就关联了以 videosys 命令的两个文件,DB文件和TTR文件。DB文件是完整的经过算法压缩处理的资料,可能是QuickHelp database文件,TTR文件则是全文索引。因为在MSLIB系统中可以通过F7、F8功能键来在文档顶层目录、章节目录、段落缩引间来回切换,所以TTR文件也包含了这部分的内容索引。TTR文件包含的内容基本都是重复DB文件的,在当年的技术条件下这还算是比较浪费磁盘空间的做法,如果采用更优化的索引技术,CDROM空间还可以节省30%以上。另外还可能有一个对应的QTR文件,在MSL中可以通过Alt+Q,即Quick Reference功能,它的作用就相当于 HTML Help 系统的索引文件。在DOS编程下,主要参考资料是以下这些:
MS-DOS 4.0 Programmer's Reference
MS-DOS 3.3 Programmer's Reference
The MS-DOS Encyclopedia
Advanced MS-DOS Programming, by Ray Duncan
The New Peter Norton Programmer's Guide to the IBM PC & PS/2, by Peter Norton and Richard Wilton
MS MASM 6.0 Reference
MS MASM 6.0 Programmer's Guide
MS MASM 6.0 White Paper
QuickAssembler 2.01 Programmer's Guide
MS Mixed-Language Programming Guide
CodeView & Utilities User's Guide
MS C 6.0 Advanced Programming Techniques
MS C 6.0 Installing and Using the P.D.S.
MS C 6.0 Reference
MS C 6.0 Run-Time Library Reference
MS C 6.0 Developer's Toolkit Reference
QuickC 2.5 Tool Kit
QuickC 2.5 C for Yourself
QuickC 2.5 Up and Running
QuickC 2.5 Update
MS Professional Advisor - Library Reference
MS Mixed-Language Programming Guide
Intel 80287 Programmer's Reference Manual 1987
Intel 80286 Programmer's Reference Manual 1987
Intel 80387 Programmer's Reference Manual 1987
Intel 80386 Programmer's Reference Manual 1986
Programmer's Guide to PC & PS/2 Video Systems
如果想要进行16-bit窗口编程,MSL也包含了详细的资料:
MS Windows 3.0 SDK Guide to Programming
MS Windows 3.0 SDK Install. & Update Guide
MS Windows 3.0 SDK Programmer's Reference Vol. 1
MS Windows 3.0 SDK Programmer's Reference Vol. 2
MS Windows 3.0 SDK Tools
MS Windows 3.0 SDK Articles
All MS Windows 3.0 SDK Manuals
MS Windows 3.0 DDK Install. & Update Guide
MS Windows 3.0 DDK Adaptation Guide
MS Windows 3.0 DDK Virtual Device Adapt. Guide
MS Windows 3.0 DDK Printer & Font Kit
All MS Windows 3.0 DDK Manuals
MS Online User's Guide
Programming MS Windows
MS Windows Sample Code
MS KnowledgeBase - MS Windows
为了得到DB数据文件压缩方法,这里使用前面介绍的IDA反解软件来对MSL.EXE主程序进行破解。首先到后面的链接下载 IDA Pro 5.0 免费版,安装后打开 MSL.EXE 主程序进行反向分析。这里主要先了解IDA最重要的主视图 IDA View-A ,因为反汇编代码就在这个视图显示,而且它不仅能显示汇编指令,还能将各个函数调以流程图形的方式组织,只需要在汇编指令代码的位置按下空格就可以切换到流程图模式。在流程图模式下,程序的指令的走向一目了然,如果说使用DEBUG32时无法给程序准确地下断点成为软件调试的主要瓶颈,那么有了程序的流程图后,就可以有针对性的给特定的指令下达断点了,断点准确度大大提升,这样调试程序就更有效率。前面讲到IDA的智能,在反解代码中就直接提示代码的功能了,比如程序的入口处就有检测DOS版本的提示信息:
+----------------------- public start E6A5 ------------------------+
|start proc near |
|mov ah, 30h ; DOS - GET DOS VERSION |
|int 21h ; Return: AL = major version number (00h for DOS 1.x) |
|cmp al, 2 |
|jnb short loc_E6BB |
+----------+-------+-----------------------------------------------+
| +---------------------------------------------------+
+----------+----------------------------------------------+ |
| mov ax, 4 | +-------+----------------+
| push ax | | loc_E6BB: |
| call sub_E9A4 | | mov di, seg seg002 |
| xchg ax, dx | | mov si, ds:2 |
| mov ah, 9 | | sub si, di |
| int 21h ; DOS - PRINT STRING | | cmp si, 1000h |
| ; DS:DX -> string terminated by "$" | | jb short loc_E6CD |
| int 20h ; DOS - PROGRAM TERMINATION | +-------+--------+-------+
| ; returns to DOS--identical to INT 21/AH=00 | * *
+---------------------------------------------------------+
流程图中,入口的块和版本不符合条件时执行的代码块,这两部分如果是C语言代码的话,它们就应该是同一个函数代码的代码,因为在汇编指令上,它们也是前后连接的。当然DOS版本检测通过时执行的代码块,也应该是同一函数的代码,即main()函数包含以上内容。因此,可以将三个流程块选中后组合成为一个功能块,按下Ctrl键拖动鼠标选中它们,然后通过右键菜单的 Group Nodes 来组块,并给它命名为 main()。组块后,它们就会合并成一个块,当然还可以再次将它们展开。反解代码经过会遇到一些无效的指令,一般的反解软件会将代码段的字符数据也一并当代机器指令来处理,这是我不能忍受的,作为反解软件连数据和指令都分辨不出来,至少也让人手动指定是什么类型的数据吧。在IDA中,基本上在分析阶段已经将字符串识别出来,如果还需要处理某些漏网之鱼,只需要在指定的内存位置按下快捷键A就可以将内容标记为字符串了。然后还有 Names, Strings, Functions 几个视图,它们列表了已经分析到的变量符号、字符串符号和函数符号,通过这些视图可以很快定位到相应的内存地址,真可谓方便至极!
有了IDA的助力,在DOSBOX下,就算用DEBUG32也可以准确地下断点了,只要多试几次,就可以定位到程序执行位置和代码在内存的相应位置。在接下来的几个块中,明显有一个代码块的CALL指令较多,应该就是主要功能区,下在E73Ah处的断点正好击中程序的主功能入口。这条CALL指令调用了 sub_10() 函数,通过函数列表,或者在流程图中双击函数名称或按回车键就可以跳转到相应的内存地址,而ESC键则可以跳回到上一个位置,这样追踪代码谁能说不是一种享受!IDA的跳转功能是个得力能手,通过快捷键G、Ctrl+L、Ctrl+P、Ctrl+E 可以很方便地在地址定位、变量名字定位、函数定位和入口定位之间选择。
通过E73A入口进入到主功能区后就壮观了,代码块和流程走线组合在一起就像是PCB电路板的布线一样,说真的PCB布线和IDA的流程图布线的算法其实是通用的。如果是玩过 Protel 的同学相信对这样的流程图就很熟悉了。话说回来,这么近百个的代码块,区区一个15寸显示屏也放不下啊,只能是一片片来处理了。流程图就不放了,想像一下田字簿的效果就知道了。通过追踪,得到以下几个重要的函数功能链,其中 sub_10() 是程序的主控中心,初看代码块好几十个,但如果将 loc_0076 分支下的条件跳转组合一下,流程图一下就简洁了,而且这部分功能在程序正常运行中根本没有执行到。而真正的主要功能在 sub_199E(),sub_024B() 一前一后分别负责文件读取、用户交互和屏幕输出处理:
start_E6A5() -> sub_10() -> loc_152() -> sub_199E() + sub_024B() Main Function
sub_29FF() -> sub_2A1F() -> sub_2CA2() -> sub_36A2() Display Function
sub_36A2()函数通过修改显示缓冲区将菜单内容打印出来,使用字符拷贝指令 movsb 将参数中的内容逐一拷贝到显示映射内存 B8000H,关于字符显示可参考后面字符输出小节。现在得到以上这个函数链表,就比较容易理清指令的具体流程了。sub_36A2() 这个函数就可以当成print()来看待,它在整个系统中使用是非常频繁的一个函数。基本上给它下断点后就可以反向追踪到任何需要输出信息的动作。而sub_2A1F()、sub_2CA2()函数就是用来设备菜单数据的代码入口。在变换菜单、选中菜单项操作中,通过流程图设下的断点又可以反向追踪得到以下流程链,对应不同的菜单功能:
1. sub_5407() -> loc_540A
2. sub_A1E9() -> sub_D4CF() -> sub_D7EC() begin menu action
3a sub_DB00() -> sub_AEEB() clear board and make a popup frame
3b sub_B1DC() -> sub_B0AE() -> sub_29FF() init viewer, popup memu, contents
3c sub_DB6D() -> sub_AFD7() -> sub_29FF() clear viewer, popup/text contents
3d sub_B0AE() -> sub_AFD7() -> sub_29FF() change menu item
3e sub_A148() -> sub_A17E() -> sub_AE7F() -> sub_AFD7() -> sub_29FF() view changed
其中 sub_D4CF() 和 sub_D7EC() 代码块头有点巨大,通过指令代码可以翻出它会调用几十个函数,而 sub_5407() 有一段循环代码用来维护菜单,入口在 loc_540A,它检测到按键时就会响应。如果是菜单变换选择就进入 loc_543C 更新菜单状态,否则进入按键等待sub_6626(),它通过 INT 28,即DOS IDLE INTERRUPT来让程序进入片刻空闲。如果确认菜单选项,结合流程图和前面已知的菜单功能代码分析,程序流程就极有可能会进入 sub_1E62(),而在后面的断点测试中,确实证实了,内容就是在执行这个函数后打印出来的,只是选择菜单后是先加载目录索引信息,在目录选中条目后才会调用它来打印内容。sub_A1E9()函数的调用意味着有新内容要输出,那么在它之前的函数 sub_54C0() 基本上就可以确认和目录内容的读取是有关的。
在追踪到 sub_2408()时,loc_2421处竟然有一条 call [bp+4],这条指令产生了代码段的切换,在IDA中竟一时不知如何处理,只好通过字节序列来查找代码的位置。在DOSBOX中的调试器显示指令地址是 3948:911E,IDA通过字节序列也没找到文件对应的偏移地址。这段代码应该是DOS系统代码,不过从代码中含有 reti 指令来看,应该是中断处理程序。它先保护现场将DS、ES段寄存器和SI、DI入栈,再将DS、ES设置为442Dh,追踪到 915B,919A, 6BF,F74 位置时输出了内容。
为了更准确得到那么部分代码处理了数据文件,这里借用SysinternalsSuite工具包中的Procmon64.exe来监视DOSBOX的动态。在 sub_199E() 阶段需要读取的文件有 BOOKCLIP.000, BOOKVIRT.MEM,然后清屏,再读入 MSL.HLP 并绘制SPLASH界面,然后读取 MSL.RSV, LIB.OVR, MSL.INI,LIB.PRC, MASM.RES 等等文件。在 sub_024B() 阶段还会读取 WRD.CAP, CUST.CLR,然后就开始程序界面的绘制。结果显示,处理资料文件的代码正是 loc_2421 这里的函数调用,它要进入DOS系统,IDA没有相关的代码提示。这些文件都是在MSL.EXE显示版权信息的过程中进行的,这个阶段还没正读入资料数据文件。在菜单出现后,选择菜单时就会读入DB文件,然后接着读入TTR文件。
在Watcom安装目录下有三个程序目录 binnt/binp/binw,对应了 Windows NT/XP、OS2、MSDOS 和 Windows 3.x/9x等系统。当前系统为 Windows 7,使用 binnt 目录下的命令工具,各工具介绍:
batserv.exe Batch Server wcc.exe C16 Optimizing Compiler
cl.exe C/C++ CL Clone for 386 wcc386.exe C32 Optimizing Compiler
ctags.exe CTAGS Utility wcl.exe C/C++16 Compile and Link Utility
cvpack.exe CV4 Symbolic Compactor wcl386.exe C/C++32 Compile and Link Utility
cvtres.exe C/C++ CVTRES Clone for 386 wd.exe Debugger
dbginst.exe Debuger Driver Install wddespy.exe DDE Spy for Windows NT
dmpobj.exe OMF Dump Utility wde.exe Dialog Editor for Windows NT
drwatcom.exe Dr. Watcom for Windows NT wdis.exe Multi-processor Disassembler
fcenable.exe Far Call Optimization Enabling wdw.exe Watcom Debuger
ide.exe IDE wfc.exe FORTRAN 77/16 Optimizing Compiler
lib.exe C/C++ LIB Clone for 386 wfc386.exe FORTRAN 77/32 Optimizing Compiler
lib386.exe C/C++ LIB Clone for 386 wfl.exe F77/16 Compile and Link Utility
link.exe C/C++ LINK Clone for 386 wfl386.exe F77/32 Compile and Link Utility
link386.exe C/C++ LINK Clone for 386 wimgedit.exe Image Editor
mkcdpg.exe WRC Code Page Generator wlib.exe Open Watcom Library Manager
ms2wlink.exe Convert MS Response Files Files wlink.exe Open Watcom Linker
nmake.exe C/C++ NMAKE Clone for 386 wmake.exe Open Watcom Make
novserv.exe Novell Remote Debug Server wpp.exe C++16 Optimizing Compiler
parserv.exe Parallel Remote Debug Server wpp386.exe C++32 Optimizing Compiler
rc.exe C/C++ RC Clone for 386 wprof.exe Excution Profiler
serserv.exe Serial Debug Server wrc.exe Windows Resource Compiler
tcpserv.exe TCP/IP Debuger Server wre.exe Windows Resource Editor
vi.exe VI Editor wsample.exe Execution Sampler
viw.exe VI Editor for Windows wspy.exe Open Watcom Spy
w32run.exe wstrip.exe Executable Strip Utility
wasm.exe Open Watcom Assembler wtouch.exe Touch Utility
wbind.exe Win386 Bind Utility
wbrg.exe Browsing Information Merger
wbrw.exe Source Browser
自带的 viw.exe 编辑器还不错,支持正则,括号匹配等等,如果可以扩展新功能如对象提示,转到定义等等就完美了。在binw目录下还自带了许多DOS工具,比如32-bit的debug命令DEBUG32.EXE,这个工具真是小巧玲珑,功能齐全,可以说是进制下的 editplus。掌握调式工具是编程必修课之一,而DEBUG则一个可以圈可点调试工具,它小巧只有几十KB,功能齐全,从读写文件到内存读写,再到CPU硬件的访问,一应具全,虽然界面简漏操作上也不够便捷,用来破解程序确实不是它的强项,而且还会非常吃力不讨好。但用它在开发时调试程序是绝对的一把利刃。在进入程序界面后,可以通过 ?a 这样的命令来获取相关的各种命令用法信息,在读写文件时,需要 n, w, l 命令配合,先通过 n 命令指定一个文件名,然后进行读写操作,写入文件时需要使用 r 命令在 BX,CX中指定字节数。通过F8、F9、F10、F11功能键,可以快捷地在运行程序、单步进入、单步跳过、输出屏幕之切换。在旧版的DOS系统中提供了纯16位的版本,现今要开发DOS程序不玩32位程序就失去DOS的最大吸引力,所以能提供这样好用又免费的工具,真的要好好感谢 Rob Larson,Larson Computing公司。
如果使用 ide.exe 则不用掌握太多的命令行内容,直接通过GUI操作就可以了。本人比较喜欢命令行办事,所以会用到 wrc.exe, wcl.exe, wasm.exe,分别用来编译和链接资源、C/C++和汇编,还有自动化编译工具 nmake.exe。典型的应用是使用 wcl.exe 来编译各种平台下的程序,wcl386.exe 则增加了新CPU的功能支持。通过指定参数 -lr 就可以编译实模式的DOS程序,它会负责调用编译程序 wcc.exe, wpp.exe, wasm.exe 和链接程序 link.exe。
Usage: wcl [options] file(s)
Options: ( /option is also accepted )
-c compile only, no link
-cc treat source files as C code
-cc++ treat source files as C++ code
-y ignore the WCL environment variable
[Processor options]
-0 generate code for 8086 or higher -4 generate code optimized for 486
-1 generate code for 186 or higher -5 generate code optimized for Pentium
-2 generate code for 286 or higher -6 generate code opt. for Pentium Pro
-3 generate code for 386 or higher
[Debugging options]
-d0 no debugging information -d2t (C++) d2 but without type names
-d1{+} line number debugging info. -d3 debug info with unref'd type names
-d2 full symbolic debugging info. -d3i (C++) d3 + inlines as COMDATs
-d2i (C++) d2 + inlines as COMDATs -d3s (C++) d3 + inlines as statics
-d2s (C++) d2 + inlines as statics
[Linker options]
-bd build Dynamic link library -k set stack size
-bm build Multi-thread application -lp create an OS/2 protected-mode pgm
-br build with dll run-time library -lr create a DOS real-mode program
-bw build default Windowing app. -l= link for the specified system
-bt= build target OS. -x make names case sensitive
通过指定-bt 和 -l 参数可以编译各种平台的不同程序格式,例如可以使用 wcl386.exe 来编译基于CauseWay扩展的DOS 32-bit 程序,编译 pharlap 扩展程序时会生成 .exp 程序格式,在DOS平台下通过 run386.exe 来运行。DOS平台的编程一定要掌握 DPMI,DOS Protected-Mode Interface Specification,通过DPMI可以解决旧式DOS内存管理,如EMS,XMS 的兼容性毛病。以下为一组编译链接和调试命令示例:
WCL386 /l=CauseWay myprog.c
WLINK system CauseWay file myprog.obj
WD /tr=cw myprog
wcl386 /l=dos4g /d2 hello.c
wd /trap=rsi hello
wcl386 /l=pharlap hello.c
run386 hello
wcl386 /l=pharlap /d2 hello.c
wd /trap=pls hello
wlink.exe 这个命令更多的时候是通过指示脚本来传入参数,上面列表中的 ms2wlink.exe 工具就是转换微软链接指示脚本用的。通过指定 system 参数,可以实现各种执行文件的链接输出:
System Description
com 16-bit x86 DOS ".COM" executable
dos 16-bit x86 DOS executable
dos4g 32-bit x86 DOS/4GW executable
dos4gnz non-zero based 32-bit x86 DOS/4GW executable
os2 16-bit x86 OS/2 executable
os2_dll 16-bit x86 OS/2 Dynamic Link Library
os2_pm 16-bit x86 OS/2 Presentation Manager executable
os2v2 32-bit x86 OS/2 executable
os2v2_dll 32-bit x86 OS/2 Dynamic Link Library
os2v2_pm 32-bit x86 OS/2 Presentation Manager executable
pharlap 32-bit x86 Phar Lap executable
tnt 32-bit x86 Phar Lap TNT executable
qnx 16-bit x86 QNX executable
qnx386 32-bit x86 QNX executable
x32r 32-bit x86 FlashTek executable using register-based calling conventions
x32rv 32-bit x86 virtual-memory FlashTek executable using register-based calling conventions
x32s 32-bit x86 FlashTek executable using stack-based calling conventions
x32sv 32-bit x86 virtual-memory FlashTek executable using stack-based calling conventions
windows 16-bit x86 Windows 3.x executable
windows_dll 16-bit x86 Windows 3.x Dynamic Link Library
win_vxd 32-bit x86 Windows 3.x or 9x Virtual Device Driver
win95 32-bit x86 Windows 9x executable
win95_dll 32-bit x86 Windows 9x Dynamic Link Library
nt 32-bit x86 Windows NT character-mode executable
nt_win 32-bit x86 Windows NT windowed executable
win32 synonym for "nt_win"
nt_dll 32-bit x86 Windows NT Dynamic Link Library
win386 32-bit x86 Open Watcom extended Windows 3.x executable or Dynamic Link Library
netware 32-bit x86 NetWare Loadable Module.
novell synonym for "netware". This is a legacy system type.
netware_libc 32-bit x86 NetWare Loadable Module.
netware_libc_lite 32-bit x86 NetWare Loadable Module.
netware_clib 32-bit x86 NetWare Loadable Module.
netware_clib_lite 32-bit x86 NetWare Loadable Module.
一个DOS 16-bit 程序的链接指示脚本,可以按如下格式编写,然后通过@将指示脚本传入,默认的脚本扩展名为 .lnk,可以指定其它扩展名:
system dos
option map
name app_name
file obj1, obj2, ...
library lib1, lib2, ...
wlink name testprog @first @second option map
要链接在Windows 3.x/9x下使用的动态载入虚拟驱动程序,可以这样设置 system:
system win_vxd dynamic
在链接 32-bit x86 Windows 3.x 扩展程序时,还需要额外的通过 wbind.exe 命令来绑定一个Watcom扩展模块,一个 .REX 文件来生成可以执行的PE程序。
wbind -n app_name
Watcom自带的自动化编译工具是 wmake.exe,也提供了微软的 nmake 自动编译工具,wmake 通过参数 -ms 就可以支持微软的脚本语法。有时需要调试脚本,可以通过 -n 参数来打印脚本生成的编译命令,而不用执行它。有了这些工具通过脚本来控制程序的编译了,以下的脚本可以保存为 makefile 文件,然后通过 wmake all 命令就可以编译所有程序,关于自动化工具的概念及使用请参考《Make自动化编译工具》,或参考官方手册。以下就是wamke命令及脚本示例:
为了增加调试信息以方便 wdw.exe 进行调试,可以增加参数 -d2 以生成所有调试符号信息,汇编命令则使用 -d1 生成调试符号信息。想要得到C语言的反汇编指令文件就使用 wdis.exe 命令来反汇编目标 .OBJ 文件。官方提供了相关的PDF文档下载,但可惜的是没有目录标签,查阅时很不方便比较浪费时在定位内容上,带目录标签的手册已经制作上传,可以通过参考资源的链接下载,版本是1.5版本,没有处理最新1.8版本的手册。CSDN的下载是要10个积分的,下载不了可以给我留言:
cguide.pdf Open Watcom C/C++ User’s Guide
cguideq.pdf Open Watcom C/C++ Compiler and Tools User’s Guide for QNX 3rd Edition
clib.pdf Watcom C Library Reference Volume 1
clr.pdf Open Watcom C Language Reference
cpplib.pdf Open Watcom C++ Class Library Reference
cw.pdf Open Watcom CauseWay User’s Guide
lguide.pdf Open Watcom Linker User’s Guide First Edition
pguide.pdf Open Watcom C/C++ Programmer’s Guide
tools.pdf Open Watcom C/C++ Tools User’s Guide First Edition
wd.pdf Open Watcom Debugger User’s Guide
vi.pdf Open Watcom Vi Editor Reference and User’s Guide
其中程序员手册很值得一看,涉及DOS、Windows和DOS/4GW大内容扩展程序等等,涉及面较广:
• DOS Programming Guide • Windows NT Programming Guide
Creating 16-bit DOS Applications Windows NT Programming Overview
Creating 32-bit Phar Lap 386|DOS-Extender Creating Windows NT GUI Applications
Creating 32-bit DOS/4GW Applications Porting Non-GUI Applications to Windows NT GUI
32-bit Extended DOS Application Development Windows NT Multi-threaded Applications
• The DOS/4GW DOS Extender Windows NT Dynamic Link Libraries
The Tenberry Software DOS/4GW DOS Extender • Windows 3.x Programming Guide
Linear Executables Creating 16-bit Windows 3.x Applications
Configuring DOS/4GW Porting Non-GUI Applications to 16-bit Windows 3.x
VMM Creating 32-bit Windows 3.x Applications
Interrupt 21H Functions Porting Non-GUI Applications to 32-bit Windows 3.x
Interrupt 31H DPMI Functions The Open Watcom 32-bit Windows Extender
Utilities Windows 3.x 32-bit Programming Overview
Error Messages Windows 32-Bit Dynamic Link Libraries
DOS/4GW Commonly Asked Questions Interfacing Visual Basic and Open Watcom C/C++ DLLs
WIN386 Library Functions and Macros
32-bit Extended Windows Application Development
Special Variables for Windows Programming
Definitions of Windows Terms
Special Windows API Functions
标准CGA字符模式下,80行25列,每个字符两个字节,屏幕每页内容占4KB,有8页内存分配在B8000H开始处。缓冲区中每两个字节作为一组,低字节位保存字符,高字节位为字符属性,保存颜色、闪动设置。CGA调色板指定16种颜色,前八种基本色为黑、蓝、绿、青、红、紫、棕、灰,后八种则是对应的高亮色,对应由0-15这几个值。字符属性字节的的低4位用来指定16个前景色,最高4-bit则用来设置背景色,但背景只有八种,因为其中最高比特位设置背景闪动属性,只有3-bit用来设置背景色。
0 black #000000 8 gray #555555
1 blue #0000AA 9 light blue #5555FF
2 green #00AA00 10 light green #55FF55
3 cyan #00AAAA 11 light cyan #55FFFF
4 red #AA0000 12 light red #FF5555
5 magenta #AA00AA 13 light magenta #FF55FF
6 brown #AA5500 14 yellow #FFFF55
7 light gray #AAAAAA 15 white #FFFFFF
使用DEBUG程序编写以下这个汇编程序就可以测试字符属性字节是什么效果:
-a 100
2358:0100 B84800 MOV AX,0048h
2358:0103 BBE001 MOV BX,01E0h
2358:0106 6800B8 PUSH 0B800h
2358:0109 07 POP ES
2358:010A 268907 MOV ES:[BX],AX
2358:010D 83C302 ADD BX,02h
2358:0110 050001 ADD AX,0100h
2358:0113 3D48FF CMP AX,0FF48h
2358:0116 72F2 JB Short 010A
2358:0118 B8004C MOV AX,4C00h
2358:011B CD21 INT 21h
-g
One problem is this: in mode 0x13, only 64K of video memory is available, even if the video card has more memory on it. Even if it is a 4MB video card, mode 0x13 can only access 64K. There is a way, however, to tweak mode 0x13 into a 256-color mode that has a total of 256K of video memory, so that page flipping is possible. This undocumented mode is sometimes referred to as “mode-x,” or “unchained mode.”
The VGA card has 256K of memory. Many SVGA cards have much more, but even on those cards, VGA modes can only access the first 256K-except for mode 0x13, which can only access 64K. The reason is that mode 0x13 is a chain-4 mode, which basically means only every forth byte of video memory is used. The reason for this is because the linear structure of the video memory allowed fast and easy video memory access. Turning off chain-4 mode allows the program to access of all 256K of video memory, but involves more complicated programming.
In unchained mode, memory exists in four 64K planes. Each plane corresponds to a specific column of video memory: plane 0 contains pixels 0, 4, 8, etc.; plane 1 contains pixels 1, 5, 9, etc.; plane 2 contains columns 2, 6, 10, etc.; and plane 3 contains columns 3, 7, 11, etc. (Figure 22). So to plot a pixel at position (5,7), plane 1 is selected, and the offset is (320*7+5)/4 = 561.
DOS可执行程序主要格式有两种,第一种是COM,这种文件结构简单,只包含机器指令和程序需要用到的数据,但一般情况下COM文件的第一条指令是JMP跳转,然后在跳转指令后接着存放数据。由于结构过于简单,甚至连重定位信息都没有,程序只能直接加载在段前缀之后,即段基址偏移0x100的位置,因为前256个字节作系统资源保留。因为在当时,通过寄存的寻址能力只有16-bit,即是64KB,通过切换段寄存器就不可能让COM程序正常运行,因为它没有地址重定位信息。因此COM程序最大只能是65,280个字节,0x100就是它的入口地址,文件结构图可以参考这里COM101。
第二种DOS可执行程序是 exe,这种文件也叫NE,New Executable,被广泛用于DOS和早期的Windows系统。这种文件格式的开头两个字节内容就是 0x4D 0x5A,字英文字母MZ,它是当时DOS系统主导开发人员 Mark Zbikowski 的名字缩写,因此这种文件又称为 MZ DOS executable。用特定的符号标记文件格式的这种做法被广泛应用,这种符号就有了一个名字叫幻数 Magic Number。MZ格式比COM要新,因此它功能上比COM要丰富,结构上也相对复杂。在文件头部包含有地址重定位信息,这使得这种程序格式可以被装载到多个内存段中。在新的Windows系统中,功能更加强大的PE格式,Portable Executable 替代了NE,PE格式的头部包含了一段代码,兼容NE,这样在旧系统打开PE文件时,这段兼容代码就被激活并输出类似以下的内容:
This program cannot be run in DOS mode.
这段代码被称为DOS stub,意思为DOS的基本代码,在编译程序时替换这段代码就可以实现新旧系统都可用的程序了。可惜当年没有对这一行字产生兴趣,不然这文章恐怕早已陈年了。运行NE程序时,它的环境变量信息会被保存在段前缀处,程序可以通过这里来找到环境变量信息,更标准的做法是通过函数来获取。NE文件的格式是以段、块来组织的,每段16个字节,每块512字节,最后一个块可能没有完全使用,可以在头部标记出来。以下是取自DJ的格式定义内容,很可惜的是MSDN竟然没有收录这样的信息:
New EXE Format
Note: all multi-byte values are stored LSB first. One block is 512B, one paragraph is 16B.
Offset Meaning
-----------------------------------------------------------------------------------------
00-01 0x4d, 0x5a. This is the "magic number" of an EXE file.
02-03 The number of bytes in the last block of the program that are actually used.
Zero means the entire last block is used (i.e. the effective value is 512).
04-05 Number of blocks in the file that are part of the EXE file.
If [02-03] is non-zero, only that much of the last block is used.
06-07 Number of relocation entries stored after the header. May be zero.
08-09 Number of paragraphs in the header. The program's data begins just after the header,
and this field can be used to calculate the appropriate file offset.
The header includes the relocation entries. Note that some OSs and/or
programs may fail if the header is not a multiple of 512 bytes.
0A-0B Number of paragraphs of additional memory that the program will need.
This is the equivalent of the BSS size in a Unix program.
The program can't be loaded if there isn't at least this much memory available to it.
0C-0D Maximum number of paragraphs of additional memory. Normally, the OS reserves all
the remaining conventional memory for your program, but you can limit it with this field.
0E-0F Relative value of the stack segment. This value is added to the segment
the program was loaded at, and the result is used to initialize the SS register.
10-11 Initial value of the SP register.
12-13 Word checksum. If set properly, the 16-bit sum of all words in the file should be zero.
14-15 Initial value of the IP register.
16-17 Initial value of the CS register, relative to the segment the program was loaded at.
18-19 Offset of the first relocation item in the file.
1A-1B Overlay number. Normally zero, meaning that it's the main program.
以上文字描述用C语言代码可以表达为:
struct EXE {
unsigned short signature; /* == 0x5a4D */
unsigned short bytes_in_last_block;
unsigned short blocks_in_file;
unsigned short num_relocs;
unsigned short header_paragraphs;
unsigned short min_extra_paragraphs;
unsigned short max_extra_paragraphs;
unsigned short ss;
unsigned short sp;
unsigned short checksum;
unsigned short ip;
unsigned short cs;
unsigned short reloc_table_offset;
unsigned short overlay_number;
};
struct EXE_RELOC {
unsigned short offset;
unsigned short segment;
};
程序数据起点偏移地址可以这样计算:
exe_data_start = exe.header_paragraphs * 16L;
在NE文件结构分解中,出现了地址重定位这个术语,什么是地址重定位呢,为何需要重定位呢?理解这些问题,产生要理解机器指令的执行过程,操作数的寻址方式,以及程序是如何从磁盘中加载到内存上运行的。例如以下一条C语言语句,通过 wcl.exe 编译后会对应产生类似后面的汇编指令,机器码在注解中给出:
int global;
int main(){
int alloc = 1;
global = 2;
return 0;
}
00401020 mov ebp,esp ; 89 E5
00401022 sub esp,00000008 ; 81 EC 08 00 00 00
00401028 mov dword ptr -04[ebp],00000001 ; C7 45 FC 01 00 00 00
0040102F mov dword ptr 00407010,00000002 ; C7 05 10 70 40 00 02 00 00 00
通过上面的指令,可以了解到编译器为 global 变量分配了一个地址,[00407010],注意程序还在磁盘中,这个地址并不是代码在内存中的地址。当操作系统将程序从磁盘中读入内存后,CPU才能执行指令,这就有个问题。假设系统分配给程序的内存地址起点是0x100,那么,原本分配给变量 global 的地址就应该更改为 0x00407110,因为起点地址和原来的不一样了。那么这个更新程序中的地址数据的过程就是重定位,当然现在的编译器不会直接用一个常量来表示一个地址,而是通过CPU的寄存器来间接定位变量的地址,alloc 变量的地址就是通过堆栈定位的。程序运行时,系统分配内存给程序,地址可以是任意的。程序运行之前会初始化堆栈、环境变量等,通过堆栈的偏移地址来间接定位变量,这样无论程序加载在什么位置都可以通过寄存器的偏移位置找定位。
这种重定位方法就是无定位依赖代码PIC,Position-independent code。PIC这种做法在代码引用数据段内容时,也会因为段地址的不同而不能做到无定位依赖,因此PIC可以节省代码段的载入时间,但数据段的加载还是需要进行重定位操作的。同时,由于增加的一条指令来实现数据的访问,因此在性能上就有所减弱。
在编译阶段,多个文件会产生多人目标文件,每个目标文件会记录下函数的当前地址,在链接时,所有目标文件会整合到一起,而函数们的地址也要对应修改符号的地址,这就称为符号重定位 Symbol Relocation。还有其它的重定位解决方法,比如说自定位 Self-relocation,也叫自修改代码Self-modifying code,通常是一些黑客常用的技术。
在Win32系统上,使用的是虚拟寻址,因此程序可以加载到编译链接时设定的基址上。有一种用于防止缓冲溢出攻击的重定位技术随机内存布局 ASLR,Address space layout randomization,通过它可以产生关键数据地址的随机性,如堆栈地址、库加载地址、可执行程序基址,这样就有效地阻止了恶意软件使用溢出攻击代码来实现准确的地址跳转。
前面讲COM可执行程序时提到的段前缀,在MSDOS系统中,这256个字节称为段前缀PSP,Program Segment Prefix的256个保留字节在操作系统加载程序时用来保存程序上下文内容。它也被称作 Zero Page,在操作系统发展早期,Gary Kildall在8位芯片上开发出了CP/M, Control Program for Microcomputers 操作系统,即微机控制程序,它就是通过 Zero Page 实现程序与系统间通讯的,这个东西早已经成为历史,x86 CPU 将内存开始的1KB保留作为中断向量表来使用。而DOS系统在加载程序时,还是会在程序所加载的段中设置PSP,对于COM程序,所以段寄存器都会指向PSP开始的位置。PSP其中包含了 MS-DOS process-termination handler、function dispatcher 等等非常重要的系统函数地址,前者负责在程序结束时开展清理工作,后者则是分发器,负责磁盘操作,输入输出及其它中间程序服务。PSP的最后 128-byte,即80H到FFH这个部分是作为默认磁盘缓冲区使用的。
0000H Int 20H
0002H Segment, end of allocation block
0004H Reserved
0005H Long call to MS-DOS function dispatcher
000AH Previous contents of termination handler, interrupt vector (Int 22H)
000EH Previous contents of Ctrl-C interrupt vector (Int 23H)
0012H Previous contents of critical-error handler, interrupt vector (Int 24H)
0016H Reserved
002CH Segment address of environment block
002EH Reserved
005CH Default file control block #1
006CH Default file control block #2 (overlaid if FCB #1 opened)
008OH Command tail and default disk transfer area (buffer)
OOFFH ----------------------------------------------------------
现在的CPU将内存开始的1KB保留作为中断向量表 Interrupt Vector Table 来使用,IVT总共可以保存256个整形指针,指向中断程序的地址。IVT的地址是不变的,这是实模式的特色,在保护模式下,中断则是通过中断描述符表IDT来管理的。0x00-0x1F 这32个中断号则是CPU保留使用的:
INT_NUM Short Description PM
-------------------------------------------------------------------------
0x00 Division by zero 0x0A Invalid Task State Segment
0x01 Debugger 0x0B Segment not present
0x02 NMI 0x0C Stack Fault
0x03 Breakpoint 0x0D General protection fault
0x04 Overflow 0x0E Page fault
0x05 Bounds 0x0F reserved
0x06 Invalid Opcode 0x10 Math Fault
0x07 Coprocessor not available 0x11 Alignment Check
0x08 Double fault 0x12 Machine Check
0x09 Coprocessor Segment Overrun 0x13 SIMD Floating-Point Exception
Ralf Brown 整理了较完整的中断功能手册,并将所有中断分成26个类别,网友还制作了GUI检索工具 RBIL Viewer 2.1,还有DOS下的检索工具 Interrup Helper 等等可以很方便地定位与查询相关的中断功能。下载Ralf Brown’s Interrupts资源后,包内有一个工具COMBINE.COM,它用来将所有分割文件 INTERRUPT.A-Z重新合拼为LST文件,这样就可以通过工具来进行浏览和查询:
A - applications, a - access software (screen readers, etc),
B - BIOS, b - vendor-specific BIOS extensions,
C - CPU-generated, c - caches/spoolers,
D - DOS kernel, d - disk I/O enhancements,
E - DOS extenders, e - electronic mail, F - FAX,
f - file manipulation, G - debuggers/debugging tools, g - games,
H - hardware, h - vendor-specific hardware,
I - IBM workstation/terminal emulators, i - system info/monitoring,
J - Japanese, j - joke programs,
K - keyboard enhancers, k - file/disk compression,
l - shells/command interpreters,
M - mouse/pointing device, m - memory management,
N - network, n - non-traditional input devices,
O - other operating systems,
P - printer enhancements, p - power management,
Q - DESQview/TopView and Quarterdeck programs,
R - remote control/file access, r - runtime support,
S - serial I/O, s - sound/speech,
T - DOS-based task switchers/multitaskers, t - TSR libraries
U - resident utilities, u - emulators,
V - video, v - virus/antivirus,
W - MS Windows,
X - expansion bus BIOSes, x - non-volatile config storage
y - security, * - reserved (and not otherwise classified)
Interrupt Vectoring
The CPU processes the INT instruction using the interrupt vector table, which, as we’ve mentioned,
is a table of addresses in the lowest 1024 bytes of memory. Each entry in this table is a
32-bit segment-offset address that points to an interrupt handler. The actual addresses in this
table vary from one machine to another. Figure 14-2 illustrates the steps taken by the CPU when
the INT instruction is invoked by a program:
• Step 1: The operand of the INT instruction is multiplied by 4 to locate the matching interrupt
vector table entry.
• Step 2: The CPU pushes the flags and a 32-bit segment/offset return address on the stack, disables
hardware interrupts, and executes a far call to the address stored at location (10h * 4) in
the interrupt vector table (F000:F065).
• Step 3: The interrupt handler at F000:F065 executes until it reaches an IRET (interrupt return)
instruction.
• Step 4: The IRET instruction pops the flags and the return address off the stack, causing the
processor to resume execution immediately following the INT 10h instruction in the calling
program.
Common Interrupts
Software interrupts call interrupt service routines (ISRs) either in the BIOS or in DOS. Some
frequently used interrupts are the following:
• INT 10h Video Services. Procedures that display routines that control the cursor position,
write text in color, scroll the screen, and display video graphics.
• INT 16h Keyboard Services. Procedures that read the keyboard and check its status.
• INT 17h Printer Services. Procedures that initialize, print, and return the printer status.
• INT 1Ah Time of Day. Procedure that gets the number of clock ticks since the machine was
turned on or sets the counter to a new value.
• INT 1Ch User Timer Interrupt. An empty procedure that is executed 18.2 times per second.
• INT 21h MS-DOS Services. Procedures that provide input–output, file handling, and memory
management. Also known as MS-DOS function calls.
1978年,Intel 8086 CPU 刚研制出来,这种16位CPU芯片使用40针的DIP封装。地址线只有20条,设计时认为地址线只要20条就够了,这样可以寻址 220=1MB 的内存。对当时,1MiB内存就像现在了1TB的概念一样,也就是这个设计导致后来CUP升级过程中,为了兼容而带来软件开发的各种问题,其中就有 A20 Gate。 当时将低端的640 kB地址空间作为程序的读写空间 RAM,称为常规内存 conventional memory,而余下的 384 kB 地址空间则作为系统保留用于BIOS、视频卡及扩展卡的映射内存。参考Intel在1978年发布的8086的数据手册中的内存组织部分 Memory Organization,手册只有区区30页,是非常易得实用的入门手册,比起一起手就来Intel 32-bit Architecture 要简单得多,毕竟后者的PDF合计上几千页啊:
Memory | Segment |
Reference | Register | Segment Selection Rule
-------------+-------------+-------------------------------------------------------------
Instructions | CODE (CS) | Automatic with all instruction prefetch.
Stack | STACK (SS) | All stack pushes and pops. Memory references
| | relative to BP base register except data references.
Local Data | DATA (DS) | Data references when: relative to stack,
| | destination of string operation, or explicitly overridden.
External | EXTRA (ES) | Destination of string operations:
| | explicitly selected using a segment override.
这时引入了内存分段的概念,即是分段内存模型。每一个16位的寄存器只能访问到216=64KB 的内存,为此就要使用一个额外的偏移值,这样就引入了16位的 CS、DS、ES、SS 段寄存器组合一个偏移值,也称为偏移地址 Offset Address,形成一个 20-bit 地址来寻址1MB内存的内存,段寄存器的低 12-bit 和偏移地址的高 12-bit 是重叠的。默认约定使用 CS 来访问代码段,DS 来访问数据段,ES 留给程序来访问额外数据段,SS 来访问堆栈。内存从第一个字节到最后一个字节都有一个唯一的号码连续的地址,称作内存的物理地址 Physical Address、有效地址 Effective Address。
1 KB 0x00000-0x003FF Interrupt Vector Table
1st 64 KB 0x00400-0x0FFFF Ordinary user memory to 64 KB (low memory)
2nd 64 KB 0x10000-0x1FFFF Ordinary user memory to 128 KB
...
10th 64 KB 0x90000-0x9FFFF Ordinary user memory to 640 KB
11th 64 KB 0xA0000-0xAFFFF Extended video memory (EGA/VGA)
12th 64 KB 0xB0000-0xBFFFF Standard video memory (MDA/CGA, Video Text)
13th 64 KB 0xC0000-0xCFFFF ROM expansion (XT, EGA, 3270 PC)
14th 64 KB 0xD0000-0xDFFFF other use (PCjr cartridges, LIM EMS)
15th 64 KB 0xE0000-0xEFFFF other use (PCjr cartridges, LIM EMS)
16th 64 KB 0xF0000-0xFFFFF System ROM-BIOS and ROM-BASIC, ROM bootstrap routine
以上为DOS环境下的内存分配,按64KB为一个分段,将一共16段分割成多块分别作不同用途。其中,1MB最高一个64KB位置作为BIOS映射内存,这是因为8086在加电初始化时,CPU首先会从0FFFF0H执行,而BIOS在这里要实现从启动盘中读取引导区程序并由引导程序来加载操作系统。
为了适应这种分段内存模型,Open Watcom 编译器采用了六种经典的DOS程序内存模型配置来管理 small, big, hug 三种内存管理模型,以下为各模型对应的代码、数据及指针的属性:
Memory Code Data Code Data Run-time Floating-Point Floating-Point
Model Model Model Pointer Pointer Library Library Library (80x87)
------- ------ ----- ------- ------- --------- -------------- ---------------
tiny small small near near CLIBS.LIB MATHS.LIB MATH87S.LIB
small small small near near CLIBS.LIB MATHS.LIB MATH87S.LIB
medium big small far near CLIBM.LIB MATHM.LIB MATH87M.LIB
compact small big near far CLIBC.LIB MATHC.LIB MATH87C.LIB
large big big far far CLIBL.LIB MATHL.LIB MATH87L.LIB
huge big huge far huge CLIBH.LIB MATHH.LIB MATH87H.LIB
表中的 samll 就代表了一个64KB内存范围,因此 near 也就是16-bit的指针。而 big 和 huge 模型都代表了可以使用 32-bit的指针,因此它们可以实现跨段的寻址。但是 big 和 huge 不的是,big 受到约束,它不可以实现超过64KB范围的寻址,尽管它可以实现跨段寻址。这是因为对于 far 指针来讲,是通过段寄存器和偏移地址实现的跨段访问,但指针的移动指向时只是修改了偏移部分,因此它和 near 一样不能寻址64KB范围以外。huge 则倾向于 Win32 环境下的 flat 模型,通过32-bit指针可以实现4GB的寻址空间。在不同的模型下,编译链接程序使用的库也不同,例如 tiny 中使用的运行库文件是CLIBS.LIB,而 medium 模式下就使用 CLIBM.LIB,文件名的后缀改变了。对于浮点运算库 MATH87S.LIB,根据编译程序的选项 -fpi、-fpi87 来指定一个库emu87.lib、noemu87.lib,后者是没有模拟代码的库,基于硬件速度会更快。使用Tiny内存模型开发COM程序时,会链接一个初始化程序模块”CSTART_T.OBJ”,文件位于286库目录下。
理解CPU这种实模式下的分段内存模式,可以解决汇编语言学习过程中许多理解的问题。汇编程序就像是内存的指令映射文件,通过 segment, ends 来定义内存段,然后在段内定义函数过程 proc, endp,最后使用 .STARTUP,global start 这些伪指令来告诉编译器程序入口在那里,MASM 则可以用 end start 来同时指定程序终结和起点。最后还有一些附加的信息如CPU型号 .8086,内存模式选择 .model small,堆栈设置 .stack 256 等等就可以组成一个完整的汇编程序。在理解CPU指令的基础上,汇编语言的学习负担主要还是在不同的汇编程序的语言学习,MASM和TASM是语法兼容的两种汇编,然而汇编不只有它们,也不可能只有它们,还有免费的 wasm, nasm。
引入分段概念后,CS、DS、ES、SS 就存储各个段的首地址的高16位,也称作段基址,偏移值则作为低16位值相加,中间重叠 12-bit,得到 20-bit 寻址能力。偏移值是16-bit数据,最大值也只有65535,因此也可以认为每个段为64KB。用冒号来表示拼接偏移量,SEGMEMT:OFFSET 这样就表示了一个20位的有效地址,也就是1MB的寻址空间。计算方法,段基址左移4位+16位偏移=20位地址,如下:
SEGMENT<<4 + OFFSET
注意,程序的代码使用这种方式能够表示的最大内存寻址为:
FFFF0+FFFF=10FFEF=1MB+64KB-16Bytes
因此,这种方法不只能寻址1M,还多余出近64KB,这部分被称做高端内存区High Memory Area (HMA),也就有了图中所示的内存分段机制模型 Segmentation Memory Model (SMM)。但硬件方面,8086/8088只有20位地址线,只能寻址1MB空间,代码如果访问 100000~10FFEF 这部分内存时就出现折回到内存开始处的64KB。CPU寻址时,系统并不认为其访问越界而产生异常,而是根据20根地址线来进行寻址,因此系统计算有效地址的时候对1M求模的方式进行的,这种技术被称为折回 Wrap-around。由此开来,对不同的内存区就形成了不同称谓,0~640KB 这部分内存就称为常规内存 Conventional Memory,用一个16-bit的寄存器就可以完成寻址;而640KB~1MiB这部分内存就称为上位内存 Upper Memory Area (UMA);而高于HMA的部分就总称为扩展内存 Extended Memory。在较新版本的DOS系统是可以通过软件控制,将HMA当作传统内存使用的,以扩大程序的内存空间。
1984年8月,当 IBM PC/AT 机推出时,AT 为 Advanced Technology,这种16位 80286 机器使用了24根地址线,寻址空间可以达到16MiB。而旧有的8086上的HMA访问折回问题在这种机器上就不存在了,HMA内存可以被程序直爽访问。但是对于原有系统开发的软件,如果直接在这种CPU上运行就可以因为地址折回现象的消失而产生兼容问题,本来程序想通过折回地址来访问低端的内存,在这种CPU上就不能正常工作了。因为机器在制造时就是在键盘控制器芯片上使用一根控制线来控制A20地址的开合,默认情况下它是关闭的,这样原有系统上的程序可以正常运行。而作为新平台下的操作系统,想要使用16MB的内存空间,或者想要进入CPU的保护模式,首先要做的工作就是在机器启动时打开A2。
在DOS系统下为了使用这些额外的内存,可以借助一些工具,如XMS, DPMI,DOS/4GW,CauseWay,通过这些工具提供的函数实现实模式到保护模式的转换,从而实现对高端系统内存的访问。
当CPU发展到386时,内部的结构发生了巨大变化,其中一点就在操作数寻址的方式上,以下引用 Intel 80386 Reference Programmer’s Manual 中的一张图来表达,80386的操作数地址方式总结,通过图例可以清楚了解到,8个通过寄存器寻址组合是非常灵活的:
Figure 2-10. Effective Address Computation
SEGMENT + BASE + (INDEX * SCALE) + DIPLACEMENT
[---]
[EAX] [EAX]
[CS] [ECX] [ECX]
[SS] [EDX] [EDX] [1] [ NO DISPLACEMENT]
[DS] + [EBX] + [EBX] * [2] + [ 8-bit DISPLACEMENT]
[ES] [ESP] [---] [4] [32-bit DISPLACEMENT]
[FS] [EBP] [EBP] [6]
[GS] [ESI] [ESI]
[EDI] [EDI]
现代x86系CPU从286开始同时具有实模式,保护模式和虚拟8086模式,而且一上电就在实模式下运行,就等同是一块8086同型号的CPU。所谓保护模式是指虚拟地址保护模式,分页机制和虚拟地址的使用避免了直接使用物理地址,使内存得到保护。虚拟8086模式是一种硬件模拟技术,通过保护模式下运行的CPU资源,可以同时模拟多个8086CPU,这样可以同时运行多个实模式任务,Windows系统下运行DOS程序时,就是通过虚拟86模式实现的。
在DOS不断升级的过程中,计算也在快速更新换代。从8086到286,计算计可用的寻址空间从1MB增长到16MB,但是16MB寻址空间需要通过CPU的保护模式下访问,对实模式下运行的程序无效。当时 Lotus、Intel 还有 Microsoft 三家公司联合制订了一种用来访问1MB以上的扩充内存的技术 EMS,Expanded Memory Specification,在这之前计算硬件上尝试使用以拔码开关的形式将扩展内存映射到上位内存的一个64KB的段中,称为换页窗口 Page Frame,通过拔码每次只能访问扩充内存中的一个64KB块。而EMS则继承了这种像老鼠屎一样发明,在上位内存UMA的384KB中开辟了一块64KB的区域,并分割成16KB的小块,这样可以映射一部分的扩充内存。对应DOS的驱动程序就是EMM386,Expanded Memory Manager。
后来又出现更先进的技术XMS,eXtended Memory Specification 通过临时切换到保护模式,并将扩充内存的数据的拷贝到上位内存或常规内存的一个块上,块大小却没有限制。这种工作方法就很像EMS,从程序的角度看是无差别的,因此XMS是兼容的EMS的一种技术实现,尽管它也不十全十美,但总比M$的EMS要好上千百倍。所以从MSDOS 5.0 的开始,升级后的EMS就是采用XMS的方式工作的。驱动程序文件为 HIMEM.SYS,这个驱动也是运行于DOS之上的窗口系统加载图形库所需要的,没有它就启动来了Windows 98,另外窗口窗口系统还提供了一个支动态加载的XMS管理程序 XMSMMGR.EXE,在Windows安装时会自动检测 HIMEM.SYS 状态来决定需不需要加载。
然而,解决问题的根本还是在保护模式,因为CPU硬件设计就是在保护模式,只有这样才能将硬件资源充分而且方便地利用起来。因此,后来在Windows 3.0上应用的DPMI,DOS Protected Mode Interface 成为DOS下进行大内存应用的必选方式。一个 DPMI 服务可以是 16-bit,也可以是 32-bit 的,更可以是通用的,分别对应了三种模式 DPMI kernel, DPMI host, DPMI server。可以通过操作系统来提供,或是DOS扩展的形式来提供,分别称为 virtual DPMI host 和 real DPMI host。而 DPMI kernel 模式则可以作为DOS扩展程序来提供,如用于游戏开发的著名的DOS/4GW,还有开源的DOS/32A,也可以是独立的程序如 CWSDPMI,CWS是指作者 Charles W. Sandmann。
ANSI.SYS - support for color text and different text resolutions
ASPIxDOS.SYS, ASPIDISK.SYS, ASPICD.SYS - all must be loaded for Adaptec SCSI drives and CDROMs to work
DOSKEY.EXE - permits recall of previously typed DOS commands using up-arrow
LSL.EXE, E100BODI.EXE, IPXODI.EXE, NETX.EXE - all must be loaded for NetWare file server drive letter access
MOUSE.EXE - support for mouse device in DOS programs
MSCDEX.EXE - support for CDROM drive access and drive letter.
SBCONFIG.EXE - support for Sound Blaster 16 audio device.
SMARTDRV.EXE - install drive cache to speed up disk reads and writes.
配置DOS时,通过在config.sys配置文件中使用 DEVICEHIGH 来加载驱动程序,或在 autoexec.bat 中使用 LH 将服务程序加载到上位内存,就可以节省更多的常规内存供应用程序使用:
DEVICE=C:\DOS\HIMEM.SYS
DEVICE=C:\DOS\EMM386.EXE NOEMS
DEVICEHIGH=C:\DOS\ANSI.SYS
DOS=HIGH
LH C:\DOS\MOUSE.EXE
LH C:\DOS\DOSKEY.EXE
LH C:\DOS\SMARTDRV.EXE