检查Windows 1.0 Hello.c

2020-05-25 22:13:10

对于那些长期阅读SoylentNews的人来说,我个人对复古计算和记录个人计算机的历史和演变感兴趣并不是什么秘密。大约三年前,我发表了一系列关于恢复Xenix 2.2.3c的文章,我早就应该写一篇新的了。对于那些从事任何编程工作的人来说,您还将熟悉“Hello World”,这是大多数(如果不是全部)程序员在其职业生涯中编写的第一个程序。

最近,我受到启发,研究了Windows 1.0版的原版HELLO.C,这是一个125行的庞然大物,人们用沉默的语气谈论它。为此,我在YouTube上录制了一段视频,让人们了解Windows1.0的编程世界,然后测试Windows到Windows10的向后兼容性。

对于那些不太喜欢看视频的人,我对这段经历的评价已经过时了,该文件的注释版本可以在giHub(https://github.com/NCommander/win1-hello-world-annotations))上找到。

不过,在我们开始讨论HELLO.C之前,关于这些古老版本的Windows还有很多要说的。像所有95之前的版本一样,Windows1.0需要预装DOS。然而,这个特定版本的Windows有一个奇怪之处,那就是当它在任何高于DOS 3.3的操作系统上运行时都会崩溃。这在一定程度上是由于可以使用SETVER解决的内部版本检查。然而,即使绕过了这个版本检查,据说运行COMMAND.COM也存在一些已知的问题。为了减少潜在的令人头疼的问题,我决定简单地安装PC-DOS 3.3,然后给Windows它想要的东西。

你可能注意到了,我没有说微软DOS 3.3。原因是DOS在当时并不是作为一个独立的产品存在的。取而代之的是,系统建造商将授权DOS OEM适配工具包,并创建他们自己的DOS,如Compaq DOS 3.3。考虑到PC-DOS是为IBM自己的PC系列构建的,它通常被认为是DOS 5.0之前版本中最“通用”的版本,而这个版本被选为我们的基础。然而,由于它的年龄,它有一些怪癖,将消失在较新的和更常见的DOS版本。

PCDOS3.3在VirtualBox中加载得很好,而且在单张720KiB软盘可引导的情况下,我立即进入了命令提示符。同样,FDISK和FORMAT可用于对硬盘进行分区以进行安装。但是,每个单独的分区限制为32 MiB。即使在那个时候,这在某种程度上是有限制的,而Compaq DOS是第一个(据我所知)取消这一限制的操作系统。运行格式C:/S创建了一个可引导驱动器,但是经常被遗忘的是IBM实际上提供了一个称为SELECT的安装实用程序。

SELECT的默默无闻主要在于它不明显的名称或用法,也不在于安装DOS实际上需要它;只需将文件复制到硬盘上就足够了。但是,SELECT确实创建了CONFIG.SYS和AUTOEXEC.BAT,因此使用起来很方便。与后来的DOS设置相比,SELECT需要使用作为参数输入的目标安装文件夹、键盘布局和国家代码进行相对神秘的调用,如果这些内容不正确,则只需输出错误即可。键入正确的符文后,选择格式化目标驱动器,复制DOS,然后完成安装。

在没有大张旗鼓的情况下,第一个障碍已经跨过,我们开始安装Windows。

安装了DOS之后,它就可以安装到Windows上了。与极简主义的SELECT命令相比,Windows1.0提供了一个专用的安装程序和一个简单的基于文本的界面。这可能是因为大多数用户会被期望自己安装Windows,而不是预先安装。

另一个有趣的怪事是,由于那个时代硬盘驱动器的稀缺性,Windows可以安装到第二张软盘上,这一点我们稍后将在Microsoft C4.0中看到。安装(大部分)进行得很顺利,尽管由于打字错误,我花了两次时间才得到一个可以正常工作的安装。输入win将我带到Windows1.0相当简约的界面。

虽然功能正常,但缺少的是鼠标支持。由于年代久远,Windows早于鼠标成为标准设备,也早于PS/2鼠标协议;开箱即支持串行和总线鼠标。解决此问题的方法有两种:

第一个是我使用的,它涉及将MOUSE.DRV从Windows2.0复制到Windows1.0安装介质,然后重新安装,从菜单中选择“Microsoft Mouse”选项。需要重新安装,因为WIN.COM是作为安装的一部分静态链接的,只包含必要的驱动程序;之后没有更改设置的选项。SDK文档详细介绍了静态链接过程,以及如何在“慢模式”下运行Windows进行驱动开发,但最终结果是一样的。如果要重新配置,则需要重新安装。

第二种选择是使用Windows 1.0的PS/2版本,这是我在制作视频后才意识到的。就像那个时代的DOS一样,Windows被授权给OEM,这些OEM可以根据自己的硬件进行调整。事实上,IBM确实为他们当时的新PS/2系列计算机做到了这一点,当时增加了对PS/2鼠标的支持。尽管是PS/2系列,但众所周知,这个版本的Windows可以在AT兼容的机器上运行。

不管怎么说,第二关已经过去了,我有了一只可以工作的老鼠。这使得探索Windows1.0变得容易得多。

如果你有兴趣尝试Windows 1.0,我建议你去PCjs.org网站,使用他们基于浏览器的模拟器来玩它,因为它已经有工作鼠标支持,不需要购买有35年历史的软件。同样,有很多关于这个版本的评论,但如果我不花一点时间谈论它,至少从技术层面来说,我将是玩忽职守的。

与稍晚一些的Windows2.0相比,Windows1.0比任何其他版本的Windows都更接近DOSSHELL,本质上是DOS的图形化插件,尽管通过深奥的魔力,它能够进行协作多任务处理。这完全是通过软件诡计完成的,因为视窗系统早于80286,并在最初的8086机上运行。COMMAND.COM可以作为基于文本的应用程序运行,然而,大多数操作系统应用程序都会启动全屏会话并控制UI。

这可能是Windows 1.0在更高版本的DOS上出现问题的原因,因为它可能会控制DOS中的内部结构,以便在没有内存保护概念的处理器上执行边界魔术。

另一个奇怪的是,这个版本的Windows并没有实际的“窗口”。相反,应用程序是平铺的,只有对话框显示为自由浮动窗口。重叠的窗口将出现在2.0中,但从API可以清楚地看出,它们至少在某个时候是有计划的。最值得注意的是,CreateWindow()函数调用有x和y坐标的参数。

我最好的猜测是,微软希望避免激怒苹果,因为苹果已经走上了与任何一家公司一样的法律战争道路,因为它过于严格地抄袭了当时新款苹果Macintosh的用户界面。与后来的版本相比,也几乎没有包含的应用程序。其中最值得注意的应用程序有:记事本、画图、写入和CARDFILE。

虽然记事本与其现代版本基本上没有变化,但Write可以被认为是Word的精简版本,在Windows95被写字板取代之前,Write仍将是主流。同样,CARDFILE也是一个数字Rolodex。在Windows3.1之前,CARDFILE一直是默认安装的一部分,并且在完全消失之前一直保留在95、98和ME的CD-ROM上。

另一方面,Paint与后来成为主流的画笔应用程序完全不同。具体地说,它仅限于单色图形,并且文件以MSP格式保存。这在一定程度上是由于当时Windows API的局限性:为了将位图绘制到屏幕上,Windows提供了独立于显示的位图(Display Independent Bitmap,简称DIB)。这些没有调色板的概念,仅限于Windows作为EGA调色板的一部分使用的8种颜色。颜色支持似乎是Windows较晚才增加的功能,似乎直到Windows3.0才完全实现。

画笔(以及后来命名混乱的Paint)实际上是由ZSoft创建的第三方应用程序,有DOS和Windows1.0版本。ZSoft画笔与Windows3.0附带的画笔非常相似,并使用了一些技术技巧来利用完整的EGA调色板。

快速查看完成后,让我们回到实际的HELLO.C,这涉及到安装SDK。

获得Windows SDK安装程序在某种程度上是一种体验。微软这个时代的大部分文档已经丢失,但微软OS/2博物馆已经扫描了一些参考活页夹的副本,SDK中的第二张磁盘既有自述文件,也有安装批处理文件,设法获得了所需的大部分必要信息。

与后来的SDK版本不同,提供编译器是程序员的责任。微软正式支持以下工具:

非官方的(未经证实的),Borland C的一些版本也可以使用,尽管这是未经测试的,除了Usenet上的一些注释之外,似乎还没有记录在案。更有趣的是,上述所有工具都是针对DOS的编译器,并且没有任何针对Windows的特定支持。取而代之的是,SDK中附带了一个替换的链接器,它可以创建Windows 1.0“NE”新的可执行文件,这种可执行文件格式在被分别替换为可移植(PE)和线性可执行文件(LX)之前,也将在早期的OS/2上使用。

为了编译HELLO.C,安装了Microsoft C4.0。和Windows一样,MSC也可以从软盘运行,尽管需要大量的磁盘交换。没有提供安装程序,相反,幸存下来的PDF有几页复制命令,并结合了对AUTOEXEC.BAT和CONFIG.SYS的编辑,用于硬盘安装。也是在这一点上,我安装了Sled,这是一个全屏编辑器,因为DOS 3.3只随Edlin一起提供。编辑在DOS 5.0之前不会出现。

经过多次磁盘馈送和一些故障排除,我设法为DOS编译了一个又快又脏的Hello World程序。MSC 4.0的另一个有趣的怪现象是它不包括独立的汇编器;MASM在当时是一个单独的零售产品。编译器排序完毕后,是时候发布SDK了。

幸运的是,提供了一个安装脚本。与SELECT一样,它需要列出一堆文件夹,但除此之外,使用起来非常简单。由于可能在1985年才有意义的原因,脚本和自述文件都在磁盘2上,而不是磁盘1上。这被确认不是标签错误,因为脚本立即要求插入磁盘1。

在返回到命令行之前,安装脚本从七个磁盘中的四个磁盘复制文件。磁盘5包含Windows的调试版本,大致相当于现代Windows的检查版本。磁盘6和7有示例代码,包括HELLO.C。

通过最后一关后,要编译HELLO.EXE并不太难。

我将从更高的层次来介绍这些,我的注释hello.c对所有这些点都做了更详细的介绍。

现在我们可以构建它了,接下来来看看16位Windows应用程序的具体构成。由于年龄的原因,第一个主要区别是HELLO.C使用K&;R C仅仅是基于ANSI C函数之前的基础上。同样明显的是,某些约定还不常见:例如,windows.h缺少包含保护。

哦,天哪,任何人在实模式下编码的祸害,近指针和远指针是许多人简单地想要忘记的一个“功能”。区别似乎很简单,近指针与C中的标准指针几乎相同,只是它指向已知段内的内存,而远指针是包括段选择器的指针。安全,对吗?

是啊,我可不这么认为。要真正理解这些是什么,我们需要细分到8086的20位内存映射中。在内部,8086是一个16位处理器,因此一次可以直接寻址2^16位内存,或总共64KB。为了打破16位存储器的障碍,人们采取了各种技巧,例如存储体切换,或者在8086的情况下,分段。

不是直接访问所有20位,而是将内存指针划分为选择器(构成给定指针的基址)和距该基址的偏移量,从而允许映射整个地址空间。实际上,8086通过使用代码段(CS)、数据段(DS)、堆栈段(SS)和额外段(ES)向系统内存提供了四个独立的窗口。

因此,在数据或函数调用在同一段中且仅包含偏移量的情况下使用近指针;它们在功能上与给定段内的普通C指针相同。远指针包括段和偏移量,8086有使用这些指针的特殊操作码。值得注意的是远调用,它自动推送和弹出用于在内存位置之间跳转的代码段。这将在稍后进行相关讨论。

HelloWndProc是对Hello窗口回调的转发声明,Hello窗口回调是Windows编程的标准功能。回调函数总是必须声明,因为Windows在从任务管理器跳转到应用程序代码时需要加载正确的段。因此才有了FAR声明。此外,Windows1.0和2.0还有其他规则,我们将在下面查看。

Windows API函数都声明为Pascal调用约定,在现代Windows上也称为STDCALL。在正常情况下,C编程语言有一个名义调用约定(称为CDECL),它主要与函数调用后如何清理堆栈有关。在CDECL声明的函数中,调用函数负责清理堆栈。这是vardiac函数(也就是接受可变数量参数的函数)工作所必需的,因为被调用者不知道有多少参数被压入堆栈。

CDECL的缺点是,它需要为每个函数调用提供额外的序言和结尾指令,从而降低执行速度并增加磁盘空间需求。相反,Pascal调用约定将清理留给被调用函数执行,并且通常只需要单个操作码来清理函数末尾的堆栈。很可能是出于执行和磁盘空间方面的考虑,Windows对此约定进行了标准化(实际上仍在32位Windows上使用它)。

If(!hPrevInstance){/*如果这是第一个实例,则调用初始化过程*/If(!HelloInit(HInstance))返回false;}Else{/*从上一个实例复制数据*/GetInstanceData(hPrevInstance,(PSTR)szAppName,10);GetInstanceData(hPrevInstance,(PSTR)szAbout,10);GetInstanceData(hPrevInstance,(PSTR)szAppName,10);GetInstanceData(hPrevInstance,(PSTR)szAppName,10);GetInstanceData(hPrevInstance,(PSTR)szAppName,10。

几十年来,hPrevInstance一直是现代Windows中的残留器官。它在程序启动时设置为NULL,在Win32中没有任何作用。当然,这并不意味着它总是毫无意义。16位Windows上的应用程序存在于共享地址空间的一般汤中。此外,Windows没有立即回收标记为未使用的内存。因此,在应用程序的生命周期之后,应用程序可能会使其自身的部分保持驻留状态。

hPrevInstance是指向这些先前实例的指针。如果应用程序仍然碰巧将其资源注册到Windows资源管理器,则它可以回收这些资源,而不必重新从磁盘加载它们。如果没有加载以前的实例,则将hPrevInstance设置为NULL,从而指示应用程序重新加载它需要的所有内容。资源是使用全局键注册的,因此尝试注册同一资源两次将导致初始化失败。

我还得到了资源可以跨应用程序共享的印象,尽管我还没有明确证实这一点。

注:大部分抄袭自雷蒙德·陈(Raymond Chen)的博客,这是一本很好的读物,解释了Windows为什么会这样工作。

另一个基本消失的概念是,内存分配被归类为应用程序的本地内存分配或全局内存分配。由于分段体系结构,应用程序具有多个堆:一个是使用程序初始化并存在于本地数据段中的本地堆,另一个是需要远指针进行访问的全局堆。

每个可执行文件和DLL都有自己的本地堆,但是全局堆可以跨进程边界共享,而且据我所知,在进程结束时不会自动释放。HEAPWALK可用于查看谁分配了什么,并查找地址空间中的漏洞。它也可以与Shaker结合使用,Shaker重新排列记忆块,试图抖动松散的虫子。这类似于更现代的工具,如Linux上的valgrind或微软的应用程序测试工具。

哦,天哪,这真是个恶臭的东西,而且完全没有必要。MakeProcInstance甚至没有进入Windows3.1,它的存在完全是因为微软忘记了他们自己操作环境的细节。为了解释,我们需要更深入地挖掘分段模式编程。

MakeProcInstance的目的是注册一个适合作为回调的函数。只有在模块文件中用MPI标记或声明为导出的函数才能跨进程边界安全调用。原因是Windows需要将代码段和数据段注册到全局存储区,以便安全地进行函数调用。请记住,每个应用程序都有自己的本地堆,该堆驻留在DS中自己的选择器中。

在实模式中,执行一个远调用以跳转到一个远指针会根据需要自动推入和弹出代码段,但数据段保持不变。因此,需要一种机制来存储查找本地堆所需的附加信息。到目前为止,这听起来是相对合理的。

问题是16位窗口有一个不变量:ds=SS…。

如果您是一名真正的模式程序员,这可能会让我清楚地知道我要做的是什么。堆栈段选择器用于指示堆栈在内存中的位置。在跨越进程边界的函数调用期间,SS也与前一个SP一起被推送到堆栈。您可能会开始明白为什么MakeProcInstance变得完全不必要了。

与函数调用不需要全局注册系统不同,应用程序只需查看堆栈基指针(BP)并从那里检索以前的SS即可。因为SS=DS,所以实际上保存了之前的数据段,不需要注册,只需更改Windows处理函数结尾和序言的方式即可。这实际上是被第三方发现的,Michael Geary发布了一个名为FixDS的工具,它重写了函数代码来做我刚才描述的事情。微软最终将他的修复程序直接整合到Windows中,MakeProcInstance作为必需品消失了。

根据Raymond Chen的博客和其他来源,16位Windows的一个有趣的方面是,它实际上是在设计时考虑到了应用程序将有自己的地址空间的可能性,而且有传言说,Windows将被移植到微软基于UNIX的操作系统XENIX上运行。目前还不清楚OS/2的Presentation Manager是否与16位Windows共享代码,尽管几个设计方面和API名称紧密联系在一起。

从16位视窗的设计和使用来看,很清楚的是,这实际上是80286上的保护模式(有时称为分段保护模式)的未来防护模式。在286的保护模式下,当处理器为32位时,Me。

..