16 comments

[ 3.2 ms ] story [ 41.8 ms ] thread
Sometimes I think that if it were the old days, I probably wouldn't have been able to program. I remember that these days we program on top of 64bit virtual addresses, but how did developers do it back then
I've been recently working with Classic Mac OS programming[0] and just that memory model (also using dealing with the lack of virtual memory using opaque handles to memory that need to be locked when used) is painful enough[1] - having to deal with segment addressing on top of that does not sound like fun. Thank god for the Motorola 68000!

[0]Made an AppleTalk chat client/server https://github.com/kalleboo/GlobalTalk-Chat

[1]The equivalent to HeapWalker I used was Metroweks ZoneRanger which was bundled with their compiler. It has a nice visualization of how fragmented the memory is https://bitbang.social/@kalleboo/116302075194704555

I actually found programming on the Mac a lot more painful due to the +/-32K max displacement for BRA/BSR, in MPW you had to manually shuffle blocks of code around to get them into the 32K jump range while in Windows the tools took care of it for you.
Early editions of Inside Macintosh implied that code segments couldn't exceed 32 KB, so you'd never run into that situation. (It's not clear whether this was an enforced limitation or just a recommendation.) Instead, developers were expected to divide their applications into multiple CODE segments, using jump table entries in the A5 world to branch between segments.

Later in the operating system's lifecycle, applications typically used a single code segment and a custom loader to apply relocations, allowing them to use JSR within that segment.

> Exports are used for application code which is externally called.

This was the magic moment for me, learning Windows 3.0 programming. The idea that my program is no longer master of it's world, but instead is just something that gets loaded and called by Windows.

Pretty good detail in this article! But what really surprises me is how some ideas just keep coming back.

When I wrote a binary translator, I ended up having to keep a translated return stack to optimize RET opcodes. That put me in exactly the same position as the Win16 kernel with regard to having to patch pointers (in case of Win16, just the segment part) on stack.

Of course I did not have the benefit of my guests calling a lock function, so I ended up having to run a garbage collection operation to determine which pointers are in use & take exceptions on now-invalidated segments. Lots of extra work that Windows didn't need: it's nice to be king :-)

If you think programming in Win16 (or whatever we want to call it), you should try teaching people to do it. I worked as a commercial trainer on C and Windows way back when - C and the Windows API were no bed of roses, but the different memory models were mind-numbing for us tutors and the poor punters, many of whom didn't know C!
In 1994 I was 2 years out of school. I'd written one windows shareware application and a whole lot of unix-y things. People were excited about the internet but most people didn't have access. Unix shell accounts via dialup were common though.

One day I was encouraged to write a Windows Sockets emulation layer for ordinary dial-up shell accounts like those offered by netcom. The idea was to allow the use of the recently released Mosaic browser without an actual internet connection. I figured sure, no problem. I'll use curl or some other tool in the shell account to do the actual fetching of URLs, transfer styles over zmodem, and simulate all the tcp/ip calls in the DLL.

I couldn't even get started. The reason is that I couldn't understand how the different Windows applications could all share memory allocated at runtime in the winsock.dll.

I asked a highly experienced ex Microsoft person, and he just said what are you talking about. There's no API to allocate shared memory.

So I gave up. 6 months later someone else did it.

Around then I realized the truth: Windows 3.1 had no memory protection at all. Specifically all global variables in DLLs were shared by default. The hard part wasn't sharing memory among users of a DLL. If anything, the hard part was having good discipline to avoid sharing it.

Since I'd only used multiuser Unix in school, and I knew Windows supported multitasking (even if only the cooperative kind), I just couldn't wrap my head around the idea that I'm multitasking operating system could exist without memory protection.

Owning reference books like Petzold, already doing C++ and TP coding on Windows 3.x, I am quite sure that the protection was there for Win16 applications in 386 Enhanced mode.

Now in regards to DLLs it all depended on which memory segments were being used, and the respective code on DllMain in regards to the thread/process attachment code and related handles.

Knowing what to search for quickly gave me this article from back in the day,

https://learn.microsoft.com/en-us/archive/msdn-magazine/2000...

Good informative article.

Win16 programming was an important formative phase in my career. There is a lot of wisdom in old solutions to thorny problems and knowing them often clues you to how one may adapt them to today's problem. For example, when CPU+GPU programming appeared i immediately imagined CPU memory accessed with "near" pointers and GPU memory accessed with "far" pointers with a switch to a pseudo-segment register.

It also conditioned a programmer to learn about various complexities involved and be careful in their programming i.e. it taught you discipline. You understood your compiler, OS and hardware better and how to write code keeping them all in mind. For example, i often say my study of embedded programming started with Win16!

Another bit of cleverness was "Thunking" between 16-bit and 32-bit code. Here is Raymond Chen on how it worked there and Why can’t you thunk between 32-bit and 64-bit Windows? - https://devblogs.microsoft.com/oldnewthing/20081020-00/?p=20...

Windows never had a global name space for dynamic symbol resolution.

IMHO one of the best design decisions they made; the Unix dynamic linking model seems absolutely like an absurd workaround in comparison.

Also, no mention of FixDS? https://www.geary.com/fixds.html

> the Unix dynamic linking model

What? It's just like static linking! Only, you know, we do it at load time. At least the filenames of the shared objects to load are included into the executable — we could instead just load and search the whole of /usr/lib in unspecified order, you know!

Not all UNIXes, Aix dynamic link model is XCOFF and quite similar to Windows.