将 DOS 应用程序移植到 Linux
只要稍加注意,普通的 DOS 应用程序就可以轻松移植到 Linux 系统。本文着眼于其中涉及的一些技术,并尝试提供一个方便的“构建工具包”,其中包含人们一直希望在 Linux 下使用的一些方便的 DOS 例程。
用 C 和 C++ 语言编写的 DOS 程序通常在各种不同的内存模型中运行,这些模型具有各自的段语义。最简单的是“tiny(微小)”模型,其中程序和数据的所有部分都从一个段引用。所有三个段寄存器(CS、DS 和 SS)都指向同一个位置,以适应处理器希望的工作方式。Linux 内核以 tiny 模式的 32 位等效模式执行程序。由于偏移量是 32 位的,而不是 16 位的,因此程序可以使用 4GB 的地址空间,然后分段才会成为问题。因此,您可以获得 tiny 模型的简单性,而没有其局限性。
因此,DOS 关键字 near、far 和 huge 对 Linux 没有意义。可以删除这些关键字,或者如果您尝试维护一个通用的源代码树,则可以添加以下行来代替
#if defined(__linux__) #define far #define near #define huge #define register #endif
gcc,即普通的 Linux C 编译器,理解 register 关键字,但代码优化器已经足够好,以至于通常情况下使用 register 是一个坏主意。
许多 DOS C 编译器都支持 inline 关键字。gcc 也支持此关键字。
gcc 支持您期望的所有 ANSI C 类型以及一些扩展。但是,普通类型的大小与 DOS 编译器的大小不同,并且在移植时经常会引起问题。以下是 Linux/i386 上大小的摘要(Linux 在其他架构上,例如 64 位 Alpha,在某些方面会有所不同)
Type Name Linux DOS time/small DOS large DOS huge char 8 bits 8 bits 8 bits 8 bits short 16 bits 16 bits 16 bits 16 bits int 32 bits 16 bits 16 bits 16 bits long 32 bits 32 bits 32 bits 32 bits pointer 32 bits 16 bits 32 bits 32 bits largest array 4GB* 64KB 64KB 640KB
* 实际上,由于某些地址空间被保留并用于其他用途,因此目前您无法获得超过大约 2GB 的空间。
DOS 程序员通常会充分利用原型来避免因传递错误类型而导致的神秘崩溃。在 Linux 下混合使用 short 和 long 通常只会导致传递的参数中出现神秘的值更改,因此养成使用原型的习惯是一个好习惯。此外,您可以通过添加编译器标志 -Wstrict-prototypes 来告诉 gcc 警告您任何没有原型的例程。C 库和系统调用的所有内容都具有原型,前提是包含正确的头文件。
GNU C 编译器是一个非常灵活的工具。虽然它的编译速度比大多数 DOS 编译器慢得多,并且(有意地)没有集成开发环境,但它具有 DOS 编译器无法比拟的广泛能力和灵活性。使用过 DJGPP 编写 32 位 DOS 扩展程序的人员将熟悉 gcc,尽管在 Linux 和 Unix 形式下,它更容易使用。
值得了解如何告诉 gcc 如何处理不同“风格”的代码。它可以通过使用 -traditional 选项成为传统的 K&R C 编译器,可以通过使用 -ansi 选项成为严格的 ANSI 编译器,或者成为 GNU C 编译器——ANSI + GNU 扩展。此外,您可以使用 -pedantic 和 -Wall 选项要求它执行广泛的健全性检查。对于典型的程序,编译器将生成大量警告,其中许多警告将深入了解潜在问题。例如,编译器将检查 printf()/scanf() 及其函数族格式字符串中的转换选项是否与它们将解释的变量类型匹配。
优化器可以通过使用 -O1 或 -O2 选项来控制优化的一般级别,也可以针对那些对速度至关重要的特殊情况按优化进行控制。优化器执行广泛的窥孔和全局优化,包括寄存器的智能分配、循环展开,甚至 RISC CPU 上的指令调度。
GNU C 编译器、链接器和调试器都在自由软件基金会提供的完整文档中进行了描述,您可以购买书籍(这笔钱将用于资助更多自由软件工作)或自行打印。
要全面介绍编译器、调试工具、make 和其他程序,还需要几篇文章。如果文档和文档查看器都已安装,则键入 info gdb、info gcc 和 info ld 应该会让您有一个良好的开端。(如果 info 程序未安装,则 Emacs 编辑器也可以用于读取 info 格式的文档。)图形用户界面爱好者可能还喜欢选择 tgdb 作为 gdb 调试器的图形前端,以及 xwpe,它是一个类似著名的 DOS C 开发环境的工具,构建在 gcc、make 和 gdb 之上。
这是针对一组特定的 DOS C 库和系统函数提出的常见问题,最值得注意的是各种文本模式窗口包、kbhit()、getch()、getche() 和字符串函数 stricmp() 和 strnicmp()。
毫不奇怪,Linux 下存在等效的功能。文本模式窗口的情况值得单独用一个章节来介绍,因此您必须稍等片刻或跳到前面。字符串函数非常简单易用。stricmp() 也称为 strcasecmp(),strnicmp() 也称为 strncasecmp()——只是命名上的差异。
键盘 I/O 例程会引起问题,因为 Unix 终端 I/O 比 DOS 终端 I/O 灵活得多,并且在 kbhit() 的情况下,让 CPU 将所有时间都花在循环轮询键盘上并不适合系统的多任务处理性质。此外,与 DOS 不同,终端模式是显式设置的,而不是由每个调用隐式设置的。存在一组例程(标准 POSIX termios 函数)来操作每个设备的控制结构。
您不能使用可能以其他方式损害机器完整性的各种特权指令。控制 I/O 设备是通过内核中的设备完成的,通过文件抽象(/dev/ 中的“特殊”文件)而不是直接完成的。如果绝对必要,可以使用 ioperm() 允许以 root 身份运行的进程访问设备(但这很危险)。任何执行此操作的代码都将是非可移植的,因此应仅用于特殊目的。mmap() 可用于等效访问 PC 上的设备内存窗口(640KB-1MB),但这对于正常使用来说同样是一个糟糕的主意。特别是,您永远不应该尝试以这种方式进行屏幕输出。
Linux 环境建立在小型、高效、协同工作的程序的基础上。因此,程序执行工具种类繁多。它们与 DOS 环境的区别非常具体。DOS 下存在的各种“交换出现有程序并生成另一个任务”工具在 Linux 中没有等效项。Linux 虚拟内存系统将自动自行决定交换出什么以及何时交换出。其次,Linux 进程执行的基本构造在 DOS 中没有等效项,即使存在执行此操作的库例程也是如此。
运行另一个进程的最简单方法是通过 system() 函数,该函数调用 shell 并向其提供命令字符串以进行解释和执行。执行所有正常的 shell 解析和重定向。这意味着如果您不希望 shell 误解任何特殊字符,则应小心传递的参数。
这是一个简单的程序示例,显示谁已登录,然后显示谁已登录到名为“thrudd”的远程计算机。
void main(int argc, char *argv[]) { system("who"); system("rsh thrudd who"); }
Linux 系统大量使用管道——一种将一个命令的输出馈送到另一个命令的方法。这不仅仅是一个 shell 工具。任何程序都可以使用 popen() 和 pclose() 调用从另一个程序读取或写入。这些函数的工作方式与 fopen() 和 fclose() 相同,只是 popen() 将程序作为其参数传递。由于 pclose() 处理由 popen() 创建的进程的终止,因此使用正确的关闭例程非常重要。
这方便地将我们带回到打印,作为我们的下一个示例。以下是一组用于打印文件的子例程
FILE *open_printer(char *printername) { char buf[256]; sprintf(buf,"lpr -P%s",printername); return popen(buf,"w"); } void print_line(FILE *printer, char *line) { fprintf(printer,"%s\n",line); } void close_printer(FILE *printer) { pclose(printer); }
与 DOS 不同,Linux 在链接器中不支持覆盖,也不支持在 C 库中加载覆盖。内存管理和虚拟内存系统会将未使用的程序段交换出内存,而无需被交换的程序的协助,并在需要时自动将其重新调入。使用覆盖也不会加快程序启动时间。程序代码是从磁盘读取的,只要有问题的代码页是需要的并且未在内存中找到驻留。
Linux 为进程执行提供的核心例程是 execve()、fork() 和 wait()。execve() 调用将正在运行的程序映像替换为另一个程序映像。原始映像被完全销毁。fork() 调用创建现有进程的新副本。副本之间唯一的区别是 fork() 返回的值(父进程中新进程的进程 ID,子进程中为 0)。最后,wait() 调用允许您等待进程完成。移植 DOS 程序时不太可能需要此级别的控制,因此此处不作介绍。
通常,内核会自动阻止程序,从而避免程序在等待 I/O 时使用 CPU 时间。可以使用额外的选项 O_NDELAY 打开设备,以指示应返回错误 EWOULDBLOCK,以指示缺少就绪数据(或用于写入缺少缓冲区空间)。当程序以这种方式使用 I/O 时,它必须非常小心,不要陷入死循环。
避免以下 DOS 风格的构造
while(1) { if(kbhit()) do_something(getch()); if(timer_expired()) time_event(); }
Linux 而是提供了非常有用的 select() 系统调用,它允许您以避免轮询的方式等待多个 I/O 事件、超时或两者兼而有之,并使内核能够避免将处理器资源分配给有问题的任务。
select() 允许您等待给定的时间,或等待直到一组文件之一有数据准备好读取或有空间写入,或等待直到该文件上发生异常情况。由于 Linux 系统将合理范围内的所有内容都视为文件,因此这非常灵活。
列表 1 显示了一个“琐碎”的示例。这是 kbhit() 的实现。为了获得完全的 DOS 行为,它假定终端已处于 raw 模式,我们稍后将讨论。否则,它将在按下 ENTER 后返回 1,此时数据在线到线 cooked 模式下可用。
眼尖的 DOS 程序员可能会想,“如果我们将程序的输入从文件重定向会发生什么?它不是键盘,但输入是可用的。” 答案非常简单——磁盘文件始终准备好读取,并且在此级别上,读取键盘和读取文件之间没有区别。程序继续运行并正常工作。实际上,您可以重定向程序以从鼠标读取输入运行,并且 select() 仍将表现一致。
Linux 下的文件 I/O 比 DOS 简单得多。DOS 模拟 Unix 低级(open()、close()、read() 和 write())和高级“stdio”工具,但 DOS C 库具有自己的 ascii/二进制感知能力,以处理回车符/换行符的差异。在 Linux 下,这些都消失了,无需担心指定这些内容(尽管 ascii/二进制规范将被接受)。Linux 下的所有 DOS 设备名称都不同。Linux 系统将其设备保存在 /dev 中。以下是一个粗略的转换图表
CON: /dev/tty LPT1: /dev/lp0 LPT2: /dev/lp1 LPT3: /dev/lp2 COM1: /dev/ttyS0 /dev/cua0 COM2: /dev/ttyS1 /dev/cua1 COM3: /dev/ttyS2 /dev/cua2 COM4: /dev/ttyS3 /dev/cua3 NUL: /dev/null
请注意,正常的打印方式是通过打印服务 (lpr) 排队作业,而不是直接写入端口。在典型的 Linux 系统上,/dev/lp* 文件受到保护,因此普通用户无法直接访问它们。
Linux 中的终端 I/O 与 DOS 中的终端 I/O 截然不同。首先,POSIX 终端系统比 DOS 更具模式化。要从单字符模式(DOS 中的 getch())切换到基于行的编辑模式,需要实际的 termios 请求,该请求提供要使用的新终端参数。此外,程序负责在运行其他程序之前和退出时恢复终端状态。如果您忘记这样做,您可能需要切换屏幕并终止该进程,或者您可能会发现 shell 会因您的终端状态而感到困惑并注销您(这也解决了问题)。
列表 2 包含一些用于管理终端 I/O 设置的示例代码。
在输出方面,Unix 程序传统上避免使用直接光标控制代码,并且不能直接写入视频内存。这样做的原因显而易见。所讨论的终端可能是不同类型的机器,位于世界不同的地方。手动处理所有不同的终端类型令人不愉快,因此提供了一个名为 curses 的库。一个更现代的库,名为 ncurses,它具有颜色支持等功能,也可用于 Linux。这个库的旧版本有很多错误,但最新版本看起来确实非常好。有关介绍,请参阅文章“ncurses:Linux 的便携式屏幕处理”,Linux Journal 第 17 期,1995 年 9 月。
ncurses 为您提供简单的输出控制、颜色(如果终端支持)、功能键和其他操作,并且与终端无关。此外,它还优化了它执行的更新,以最大限度地减少慢速网络或串行链路上的流量。它是免费的,并附带一套不错的示例和良好的文档。由于它是 System V curses 的实现,您可以从图书馆借阅一本关于 curses 的书,并将其用作参考或教程(视情况而定)。
如果您决定使用 ncurses 来进行输出,它还将通过函数 cbreak()、nocbreak()、echo() 和 noecho() 提供执行 DOS 风格的逐字符输入所需的所有例程。ncurses 文档解释了所有这四个函数。
直到最近,文本模式下鼠标还没有标准的控制 API(在图形模式下,X-Windows 运行鼠标并提供您能想象到的 GUI 的所有功能)。正常的鼠标行为是提供文本模式的剪切和粘贴。这由一个名为 selection 的程序管理,最近由 gpm 管理。
gpm 库允许您的文本模式应用程序处理控制台和 X-Windows 下 xterm shell 窗口中的鼠标事件。文章“编写鼠标敏感应用程序”第 17 期,1995 年 9 月中解释了如何编写使用 gpm 控制鼠标的程序。
有时您必须移植 DOS 终止并驻留内存 (TSR) 程序。如果您一直习惯于使用未记录的 DOS 调用并切换堆栈和其他可怕的汇编语言操作,您会很高兴知道您可以忘记这段经历。
首先,终止并驻留内存的整个概念已经消失了。当程序退出时,其所有资源都会被释放,并且该进程将不复存在。这并不意味着不存在相同的功能;它们以不同的方式存在,这些方式更适合已经进行多任务处理的系统。
DOS 下 TSR 程序主要有三个原因。
提供一个子例程库,以支持某些扩展功能。几个可加载的图形库都使用了此功能。在 Linux 下,您可以创建一个新的共享库,它将可用于与应用程序链接并在多个用户之间共享。
添加设备驱动程序。设备驱动程序是内核代码。移植 DOS 设备驱动程序几乎肯定是一次重大的重写。Linux 还通过 modules 支持具有可加载的设备驱动程序。移植 DOS 设备驱动程序绝对超出了本文的范围。在某些情况下,驱动程序可能正在添加一个高级功能,该功能可以作为库或作为一直运行的实际程序提供。
创建基于弹出“热键”的迷你应用程序,如电话簿。Linux 下没有这些的理由。您有多个控制台屏幕,即使在具有 iscreen 程序的相当简单的终端上也可以拥有多个屏幕的能力,并且可以随时运行任何应用程序。因此,无需将迷你应用程序小心地修补到内核中。您可以将其作为普通程序移植。
对于第二个示例,某些 TSR 程序可以像提供服务的应用程序一样移植。gpm 鼠标管理就是这方面的一个很好的例子。它以在后台运行的应用程序和与服务器接口的支持例程库的形式提供了 DOS 鼠标服务中断设施的核心等效功能。
图形程序的移植要复杂得多,因为图形硬件接口不可用。您可以通过两种方式来处理这个问题。首先,svgalib 提供了移植应用程序所需的基本功能。请注意,您不能使用 BIOS 功能,因为它们仅在 16 位模式下可用。svgalib 应用程序可以非常快(有关出色的示例,请参阅 linuxsdoom),但无法远程运行,并且不容易移植到基于 PC 的系统之外的系统。
第二种方法是使用 X-Windows。这使得移植难度更大,因为您需要转移到基于事件的范例(类似于为 MS Windows 编程),并将您的界面对话框和菜单重写为 X 小部件。此外,X-Windows 编程——至少最初是这样——很难掌握要领。但是,结果是一个图形程序,它是可移植的并且可以远程运行。正如人们可能预期的那样,X-Windows 通常比原始 SVGA 慢。但是,有一个扩展(称为 Xshm)Linux 和大多数 Unix 系统都包含该扩展,它支持快速位图更新,这在游戏中很常见。
在“作弊框”中,还有 Tcl/Tk,这是一种用于编写简单 X-Windows 界面的前端语言。它对给定程序的适用性很难概括。但是,基本上是模态的应用程序通常可以最好地利用 Tcl/Tk。文章“从 C 程序中使用 Tcl 和 Tk”,Linux Journal 第 10 期,1995 年 2 月中介绍了如何使用 Tcl/Tk 为程序编写前端。
Alan Cox 自 0.95 版本以来一直致力于 Linux,当时他安装它是为了进一步研究 AberMUD 游戏。他现在管理 Linux 网络、SMP 和 Linux/8086 项目,自 1993 年 11 月以来一直没有在 AberMUD 上做任何工作。在现实生活中,他为 I2IT 破解 ISDN 路由器。
Borland 的 BGI 库的商业克隆也适用于 Linux。如果您的程序使用 BGI 图形,这可能是一个有吸引力的选择。在您阅读本文时,应该可以在 sunsite.unc.edu 的 /pub/Linux/apps/graphics/bgi_library.tar.gz 文件中找到共享软件版本(15 美元注册费)。
sunsite.unc.edu ftp 站点还提供了各种数据库工具,从用于处理读/写 PC 风格 XBase 文件的简单库到 SQL 系统。
一个名为 FlagShip 的商业软件包可用于将 clipper 数据库程序直接移植到 Linux。演示版本可从 ftp://ftp.wgs.com/pub2/wgs/Filelist 获取