In systems as complex as they are in modern information technology, built as fast as the IT industry demands, you would expect there to be some oddities around. Places where the builder had made a quick patch and then forgotten to go back and implement a proper fix. Or where the quick patch took on a life of its own. This is one of those. I mentioned it briefly in my eulogy of Dennis Ritchie and here is a more in-depth article: A description of the oddest little function I've ever come across.
In order to understand the situation, we must first take a look at computer memory. It is, as you would expect, where stuff gets stored. Without going into too much detail, I'll just state that each thing that is held in a computer's memory has an address. The address is a numeric value, and again without losing too much, we can say that the address is the number of bytes to count from some starting point. For example, if something had the address 100, the first byte of that object would be 100 bytes from the start of the memory area. The second byte would be at address 101, and so on.
In the process of figuring out how to run multiple programs simultaneously on a computer, there arose a need to somehow protect the programs from each other. A program that had unlimited access to memory could, by mistake or on purpose, overwrite another program - either sending it crashing, or making it do things it was not supposed to do.
2. Enter Protected Mode
The result was protected mode. The memory addresses were roughly the same, but now they had a selector, which can be thought of as a number indicating which isolated compartment in memory the address was referring to. An address with a selector could look like this:
1234:0100, which meant offset (which was the new name for "address", as it indicated the offset from the start of the compartment) 100 in the compartment denoted by selector 1234.
In this way, the "compartments" could be given access control features. For example, we could specify that the compartment 1234 could only be used by a certain program. If any other program tried to access that compartment, the processor could instantly terminate it, thus protecting the contents of the compartment.
So we had a 13-bit selector, which gave the computer a total of 8192 different compartments, and a 16-bit address part, which meant that each compartment could be up to 65536 bytes.
3. An Abundance of Memory
The problem that now arose was that when memory became larger and larger, the users started wanting to process bigger and bigger items of data. Where before 64kb was quite enough for an image, the new multi-color displays required computers to process images of several hundreds of kilobytes. (As a comparison, as of the time of my writing this, images from a cell phone camera take up fifteen megabytes, or fifteen thousand kilobytes, an increase by a factor of one hundred thousand.)
There was a need to split large slabs of data across several of these compartments. Preferably in a predictable way.
The solution was to let each compartment used in splitting the block have a selector that differed from the previous block by a fixed amount. If the selector for the first 64kb block was 1234, then the next 64kb block would be at selector 1234+X, the third block would be at 1234+2*X, and so on.
The solution was simple, but how would you communicate to the program which value it would use for X? One option would be to declare it in a standard: The industry heavyweights get together and declare that X will always equal, say, one. Or two, or three. The point isn't the actual number, it is that it is declared from on high and everyone just gets with the program. Another option would be to have a function, for example called
GetSelectorIncrement, that would return the value that X had on the given system. That's fine, too, and is the way a program can find out how big the screen is, and other useful bits of information about its environment.
The choice that we ended up with was none of the above. It is a method that, to my knowledge, has only been used once.
4. Enter AHIncr
The solution was to declare a function as "magic". The choice fell upon the
AHIncr function, that when called, would increment the processor's AH register (register A, High half) by one. This function, I might add, has absolutely nothing to do with addressing. What the function does have is an address, just like every other object in the computer's memory. That address would, like everything else, be on the form
selector:offset. Normally, this address is completely without any meaning other than pointing to the start of the function in memory. Both the selector and the offset value can be anything. This time, however, the run-time linker would guarantee that the
offset part of the address was precisely equal to the difference in selector values for adjacent 64kb blocks.
Did you follow that? Let me repeat: If your first block of memory is at
1234:0000, then the second would be at
1234 + offset(AHIncr):0000. The third would be at
1234 + 2 * offset(AHIncr):0000, and so on. In general, if you wanted to access byte
N from a sequence of blocks starting at
firstSelector:0000, then you'd be in block
b which would be number
N / 65536 (since each block is 65536 bytes), and the
offset in that block would be
N modulo 65536 (again because each block is 65536 bytes), giving you an address of
firstSelector + (b * offset(AHIncr)):offset.
This is, to my knowledge, the only time when the address of a function has had any other meaning than as an opaque pointer to the start of the function.
procedure AHIncr; far; external 'KERNEL' index 114;
Then you know that the program has no intent of incrementing the AH register. It just wants to work with blocks larger than 64kb.
5. How It Worked
To sum it up, I'll briefly outline how memory blocks larger than 64kb were handled.
procedure AHIncr; far; external 'KERNEL' index 114;
GlobalAllocto allocate the memory, since Turbo Pascal's allocator only could allocate a maximum of 64kb in a continuous block.
Then you call
GlobalLockto pin the slab you allocated in the previous step and keep the operating system from moving it around. This gave you an address to the start of the block.
This address would always be on the form
xxxxbeing the selector for the first 64kb block.
Then you process the data in that block, add
Ofs(AHIncr)to the selector part and set the offset part to zero, and process the next block.
Repeat until done, maybe making several passes.
GlobalUnlockto "unpin" the block and allow the operating system to move it around if needed.
GlobalFreeto release the memory back to the global memory pool.