AHIncr: The Oddest Little Magic Function
 
Support Ukraine

AHIncr: The Oddest Little Magic Function

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.

1. Addressing

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 in Turbo Pascal for Windows[a] 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 have a "magic" function that wasn't even a function - AHIncr. The reason I write "wasn't even a function" is because AHIncr is a variable[b] that had to be imported into the Turbo Pascal program as a function. When importing a variable as a function, the value of the variable - instead of its address - ends up being the address of the imported function. This address would be on the form selector:offset, and normally be without any meaning other than pointing to the start of the function in memory. This time, however, since we simply took the 16-bit value of AHIncr-as-a-variable and wrote it in the 32-bit field of AHIncr-as-a-function, 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.

So, if you see old Turbo Pascal for Windows code, like this unit for loading and storing bitmap images[c], and you come across this line:

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 in Turbo Pascal for Windows.

  1. Import AHIncr from the KERNEL module:

    procedure AHIncr; far; external 'KERNEL' index 114;
  2. You'd call GlobalAlloc to allocate the memory, since Turbo Pascal's allocator only could allocate a maximum of 64kb in a continuous block.

  3. Then you call GlobalLock to 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.

  4. This address would always be on the form xxxx:0000, with xxxx being the selector for the first 64kb block.

  5. 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.

  6. Repeat until done, maybe making several passes.

  7. Call GlobalUnlock to "unpin" the block and allow the operating system to move it around if needed.

  8. Call GlobalFree to release the memory back to the global memory pool.

2014-03-01, updated 2020-06-07