First, let me point out that this guide is badly named - it should be called "Brendan's Guide To Generating a Physical Address Space Map". I chose to use the wrong name so that it gets noticed by people who need it...
The goal here should be to build a map of the physical address space, which is more than just detecting installed RAM or detecting usable RAM. The reason for this is so we know which areas in the physical address space are free and can be used for memory mapped PCI devices; but it's nice to have a complete map for other reasons too.
I'll also assume you want to do a good job; and don't want to be lazy and do a bad job, and then expect the end-user (who may not even know what "memory" is) to fix up your mess using kernel parameters or something.
Framework
Before we start, I'll assume there's an "add a new entry to the physical address space map" function, which adds a new entry into a list; and also takes care of fixing unknown area types, sorting the list, removing areas with zero size, handling overlapping areas, combining adjacent areas of the same type, etc. I'll also assume there's several different detection routines that are called one at a time until one of them returns "success". This makes it easy to insert new detection routines (or enable/disable detection routines as needed). I'll also assume you keep track of which detection routine was used so software can find out later, but that's optional I guess.
INT 0x15, EAX=0x0000E820
This is the best function to use, but there's 2 versions of it. There's the original version which returns 20-byte entries (size, length, type) and the new version (introduced with ACPI 3.0) that returns 24-byte entries (size, length, type, flags). These new flags include whether or not the area is volatile or non-volatile (so that an OS knows if it needs to save the area's contents somewhere or not, for certain sleep states) and a new "ignore this entry" flag. If (ACPI 3.0) 24-byte entries aren't supported then you won't get these flags, which IMHO is a bit dodgy (it's bad design from ACPI) because you won't get the "ignore this entry" flag either. AFAIK it's safe to assume that people who write BIOS code are smart enough to return "size = 0" when the entry being returned isn't valid (in addition to the "ignore this entry" flag). ACPI 3.0 also defined a new "faulty RAM" type, but you should still see this (if there's faulty RAM present) if you ask for 20-byte entries.
INT 0x15, EAX=0x0000E820 should return the following areas:
- RAM starting at 0x00000000, excluding the EBDA (which will be correctly reported as a "system" area), but including the BDA (which is usable RAM if your OS doesn't use the legacy BIOS).
- More "usable RAM" areas, including RAM above 0x100000000 (if any).
- Several "system" areas - typically including one entry for the EBDA, one entry for the legacy BIOS area (e.g. from 0x000F0000 to 0x000FFFFF) and one entry for the BIOS ROM itself (e.g. from 0xFFFC0000 to 0xFFFFFFFF); and possibly including some more areas for I/O APICs and local APICs.
- None or more "ACPI Reclaimable" areas. These are RAM that's used to store ACPI tables. When the OS has finished with the ACPI tables it can use these areas just like usable RAM. Typically there's one "ACPI reclaimable" area near the top of RAM.
- None or more "ACPI non-volatile storage" areas. These are RAM that's used to store ACPI data. Unless you're working on code to handle ACPI sleep states you can just treat these areas the same as "system" areas (don't touch them). Typically there's one "ACPI non-volatile storage" area near the top of RAM.
- None or more "faulty RAM" areas (ACPI 3.0 only). These are RAM that's faulty (but you probably guessed that). Hopefully there isn't any of these, but if there is don't use them.
- The BDA. As far as "INT 0x15, EAX=0x0000E820" is concerned, the BIOS Data Area is usable RAM.
- All memory mapped devices. This includes the legacy video display memory area (from 0x000A0000 to 0x000BFFFF), the video card's linear frame buffer (if any).
- Option ROMs. These are the ROMs for the video card, SCSI controllers, etc that are normally in the area from 0x000C0000 to 0x00E0000.
- Some BIOSs expect the function number to be EAX=0x0000E820 (rather than AX=0xE820), so make sure the highest 16 bits of EAX are clear before calling. [source: Ralph Brown's Interrupt List]. Note: It's because of this bug that I always refer to this BIOS function as "INT 0x15, EAX=0x0000E820", rather than as "INT 0x15, AX=0xE820"...
- For the last entry, some BIOSs clear the continuation value (EBX = zero) when returning the last entry, and some BIOSs set the carry flag for the entry after the last entry. [source: Ralph Brown's Interrupt List]
- Some BIOSs trash EDX. Just ignore the returned value in EDX and reset it to 'SMAP' before calling the BIOS again. [source: Linux]
- If the BIOS does set the carry flag for the entry after the last entry (instead of returning "EBX = zero" for the last entry), then the returned value in EAX may or may not contain the 'SMAP' signature. [source: Linux]
- Some BIOSs stop returning the 'SMAP' signature in EAX, half-way through the list of areas. In this case it may be best to forget about "int 0x15, eax = 0x0000E820" and discard everything it returned so far; or maybe refuse to boot and display a big message explaining why (so you can figure out which computers have this problem and investigate further). [source: Linux].
- It's common for BIOSs to return zero length entries. Just ignore them. [source: Personal experience]
- It's common for BIOSs to return entries in an unsorted order. It's a good idea to sort them (e.g. insert new entries into the correct place in your list, rather than adding them to the end of your list). [source: Personal experience]
- There's some doubt about whether or not some BIOSs return overlapping areas, which means it's best to assume that some BIOSs do return overlapping areas. [source: Linux - doubt arises from not knowing if this is done because of dodgy BIOSs or if it's done because users can add their own potentially messed up entries using kernel parameters]. There's 2 ways of handling this situation if the overlapping areas aren't the same type - change the type of the overlapping area to something unique (e.g. "TYPE_MIXED"), or have a system of priorities where certain area types take precedence over other area types (e.g. if an area is reported as usable RAM and as "system", then assume the overlapping area is of the "system" type). I prefer the former technique because it makes it obvious (e.g. when you're displaying the physical address space map to the user) that there was confusion. Linux uses the latter technique.
- Some BIOSs always return 20 bytes regardless of how big (or small) your buffer is (the "buffer size" value you put in ECX before calling the function may be ignored). Because of this, never ask for less than 20 bytes. [source: Ralph Brown's Interrupt List]
- To detect if 24-byte entries are supported by the BIOS (including the new flags introduced by ACPI 3.0), tell the BIOS your buffer is at least 24 bytes (ECX >= 24 before calling) and see if the returned value in ECX (the actual length returned in bytes) is 20 or 24. In addition, I'd be very tempted to set the "flags" field in the buffer to 0xFFFFFFFF and check that this value actually was changed by the BIOS, just in case the BIOS only returns 20 bytes but doesn't change ECX (maybe I'm paranoid, but maybe my paranoia is justified).
The Illusion Of Sanity Ends Here
You need a map of the physical address space, but "INT 0x15, EAX=0x0000E820" is the only BIOS function capable of returning this information and it's not supported on older computers (and even if it is present there's a tiny chance that it might be too broken to rely on). The solution to this problem is to use a variety of older BIOS functions that mainly only return usable RAM, and then construct default "system" areas around this information so you end up with a conservative physical address space map that's roughly equivalent to what you would've got from "INT 0x15, EAX=0x0000E820".
Each of these older BIOS functions have their own limits and bugs, and each of them (except "INT 0x12") may not be present or might not return usable results. I'll go through each of these BIOS functions and then give some information about manually probing RAM.
INT 0x12
This function returns the number of KiB of usable RAM starting at 0x00000000. It's the oldest BIOS function, but it's also the most reliable BIOS function - it *always* works (hmm, except for one specific case). Because this function only returns RAM at 0x00000000, and because all the other "legacy" BIOS functions only return RAM above 0x00100000, it's a good idea to always use this function to create a "usable RAM" area in your physical address space map (regardless of which other "legacy" BIOS functions you end up using).
The only case where "INT 0x12" doesn't necessarily work is for PXE/netboot code, where the networking layer takes up part of the RAM below the EBDA. In this case "INT 0x12" returns the amount of RAM left for you, and you can get the real amount of RAM at 0x00000000 from PXE data structures. See the PXE specification for more details...
After creating a "usable RAM" entry from the "INT 0x12" results, I'd also create a "system" area that starts at the end of this RAM area and ends at 0x000A0000, to reflect where the EBDA would be. For example, if "INT 0x12" returns "ax = 638", then create an entry for a "usable RAM" area from 0x00000000 to 0x0009F7FF, and then create an entry for a "system" area from 0x0009F800 to 0x0009FFFF.
In addition, create a similar entry for the legacy BIOS ROM area. You know this "system" area will end at 0x000FFFFF, but unfortunately you don't know where it should really start - does the legacy BIOS start at 0x000E0000, or 0x000F0000, or something else? Fortunately it doesn't matter - just assume it starts at 0x000C0000 (even though you know it doesn't start here, it's a safe default).
Finally, create another entry for a "system" area that ends at 0xFFFFFFFF. This is for the real BIOS ROM (rather than the piece of the BIOS copied to the RAM in the legacy BIOS area). This area is a little trickier - there might be a 2 MiB ROM from 0xFFE00000 to 0xFFFFFFFF, or it might be smaller, or larger. There might also be I/O APICs (typically at 0xFEC00000) and local APICs (typically at 0xFEE00000). I'd just create an entry for a 32 MiB "system" area from 0xFE000000 to 0xFFFFFFFF.
Now that these system areas are done the only thing you're missing is entries for usable RAM areas for any RAM above 0x00100000. There probably shouldn't be any "ACPI reclaimable" or "ACPI non-volatile storage" areas, because if the computer supported ACPI then "INT 0x15, EAX=0x0000E820" would've been supported. If "INT 0x15, EAX=0x0000E820" was supported but failed then it might cause problems, but it's extremely unlikely that any computer that supports ACPI would also have an unusable implementation of "INT 0x15, EAX=0x0000E820".
INT 0x15, AX=0xE881
This function returns the amount of extended memory between 0x00100000 and 0x00FFFFFFF (in EAX in 1 KiB blocks), the amount of extended memory above 0x01000000 (in EBX in 64 KiB blocks), the amount of configured memory between 0x00100000 and 0x00FFFFFFF (in ECX in 1 KiB blocks), and the amount of configured memory above 0x01000000 (in EDX in 64 KiB blocks). Nobody seems to know what the difference between "extended" and "configured" is. In general, use the "extended memory" values unless these values are zero (use the "configured memory" values if the "extended memory" values are zero). If this function returns with carry set then it's not supported. For additional sanity checks, I'd check to make sure that the amount of memory between 0x00100000 and 0x00FFFFFFF is not greater than 15 MiB, and that the amount of memory above 0x01000000 is not greater than about 2032 MiB.
[EDIT]
WARNING: This BIOS function hangs on some systems. I've got a Pentium system here that hangs when this function is called, even if the function is called very early during boot (before the boot loader does anything else). Some research indicates that Pheonix introduced this function with PhoenixBIOS version 4.0. The BIOS in my dodgy computer is PhoenixBIOS version 4.1. I'm not sure if the problem is caused by the BIOS itself, or some sort of incompatibility between the BIOS and the network card's ROM (network card ROMs typically intercept the BIOS memory size functions). I'm currently favoring the latter (incompatibility between the BIOS and the network card's ROM) because I've had this computer for a while and I would have tested this BIOS function on it in the past, but the network card was added recently. I also noticed I'm not the only person to have trouble with this BIOS function (for e.g. according to change logs, the Netboot project removed support for this function in March 2003 because some BIOSs hang).
[/EDIT]
INT 0x15, AX=0xE801
This function returns the amount of extended memory between 0x00100000 and 0x00FFFFFFF (in AX in 1 KiB blocks), the amount of extended memory above 0x01000000 (in BX in 64 KiB blocks), the amount of configured memory between 0x00100000 and 0x00FFFFFFF (in CX in 1 KiB blocks), and the amount of configured memory above 0x01000000 (in DX in 64 KiB blocks). Nobody seems to know what the difference between "extended" and "configured" is. In general, use the "extended memory" values unless these values are zero (use the "configured memory" values if the "extended memory" values are zero). If this function returns with carry set then it's not supported. For additional sanity checks, I'd check to make sure that the amount of memory between 0x00100000 and 0x00FFFFFFF is not greater than 15 MiB, and that the amount of memory above 0x01000000 is not greater than about 2032 MiB.
INT 0x15, AX=0x8A
This function returns the extended memory size in DX:AX in KiB, or to be more specific, it returns the number of contiguous KiB of usable RAM starting at 0x00100000. This is also where it starts getting tricky...
If the ISA memory hole is present (which is a 1 MiB hole from 0x00F00000 to 0x00FFFFFF used by ISA devices for memory mapped I/O - e.g. an ISA video card's linear frame buffer) then this function might not report all usable RAM. For example, it might report RAM from 0x00100000 to 0x00F00000 and wouldn't be able to report any RAM above 0x01000000 (if present).
Basically, if this function says there's 14 MiB of RAM at 0x00100000 then you can't assume there isn't more RAM at 0x01000000. In this case, it's likely that none of the other methods will be able to tell you more, so you'd probe for any extra memory starting at 0x01000000.
If this function isn't supported it'll return "carry = set".
INT 0x15, AX=0xDA88
This function returns the number of contiguous KiB of usable RAM starting at 0x00100000 in Cl:BX in KiB. This is very similar to "INT 0x15, AX=0x8A" - if this function says there's 14 MiB of RAM at 0x00100000 then you can't assume there isn't more RAM at 0x01000000, so you'd probe for any extra memory starting at 0x01000000.
If this function isn't supported it'll return "carry = set".
INT 0x15, AX=0x88
This function returns the number of contiguous KiB of usable RAM starting at 0x00100000 in AX in KiB. Like some other functions, if this function says there's 14 MiB of RAM at 0x00100000 then you can't assume there isn't more RAM at 0x01000000, so you'd probe for any extra memory starting at 0x01000000. In addition, for some BIOSs the value returned is limited to 15 MiB, so if the value returned is 14 MiB or 15 MiB you'd probe for any extra memory starting at 0x01000000.
This function is limited to 65535 KiB of RAM - it can't return a higher number. Because of this, if the value returned is 0xFFFF or 0xFC00 (the largest amount rounded down to the nearest whole MiB) then you might need to probe for any extra memory above the reported RAM.
If this function isn't supported it should return "carry = set", but it might return an status code in AH - "ah = 0x80 (invalid command)", or "ah = 0x86 (unsupported function)". I'd be a little paranoid and treat "ah = 0x88" (AH unchanged) as an error too. This can cause problems if there actually is 0x8000 KiB of RAM but that's unlikely, as most systems don't have 33 MiB of RAM (1 MiB at 0x00000000 and 32 MiB at 0x00100000).
CMOS Locations 0x17 and 0x18
These CMOS locations contain the number of KiB of RAM starting at 0x00100000. If the value is zero, then assume these CMOS locations are wrong.
If the ISA memory hole is present (a 1 MiB hole from 0x00F00000 to 0x00FFFFFF) the CMOS might ignore it and tell you there's RAM there, so to be safe always assume that the ISA memory hole is present.
Like "INT 0x15, AX=0x88" the value from CMOS may be limited to 16 MiB, so if CMOS says there's 14 MiB or 15 MiB you'd probe for any extra memory starting at 0x01000000; and if the value returned is 0xFFFF or 0xFC00 (the largest amount rounded down to the nearest whole MiB) then you might need to probe for any extra memory above the reported RAM.
For an example, if the value is 0x3C00 then you'd truncate this (in case there's an ISA hole) and add an entry to your list of physical address ranges for usable RAM from 0x00100000 to 0x00E00000, and then probe for more RAM starting at 0x01000000.
Probing
Lots of things can go wrong with probing. The problems can be split into 3 categories...
The first problem is detecting unusable RAM as usable RAM. This includes the EBDA, any ACPI areas and any faulty RAM. The second problem is detecting memory mapped I/O as RAM. There's 2 likely situations here - an ISA device in the ISA memory hole and a PCI device just above the top of RAM. The third problem is bad probing technique, where you get "false positives" and think there's RAM at an address when there isn't anything at all.
If you've already tried all the BIOS functions, etc above, then you know the computer is old and doesn't support ACPI, and it's very likely you won't need to probe for any memory between 0x00100000 and 0x00FFFFFF. Because "INT 0x12" always works you'll never need to probe for RAM between 0x00000000 and 0x000FFFFF, and won't need to worry about the EBDA.
If you do need to probe for RAM between 0x00100000 and 0x00FFFFFF, then assume the ISA hole is present and only probe for RAM between 0x00100000 and 0x00EFFFFF. For PCI devices, usually they're mapped into higher addresses, so for an older system it's unlikely that there's enough RAM (and enough memory mapped PCI devices) that they meet in the middle.
That only leaves "false positives". To avoid this you need good probing code. Some general tips are:
- Don't assume the area isn't being cached. To avoid testing if the cache is present (instead of testing if RAM is present) flush the cache (WBINVD). For 80386 the WBINVD instruction isn't supported, but 80386 CPUs often didn't have any caches as the memory ran at the same speed as the CPU so you could just skip the WBINVD (or alternatively, skip the 80386 - not much point supporting them anymore).
- Some computers exhibit "bus float", where capacitance between data lines on the bus are capable of "remembering" a data value, so that a write may be read back correctly even though nothing is connected at that address. To avoid this, do a dummy write to a different address (using different data) between writing to the test address and reading from the test address.
- Probe at the end of each block instead of at the start of each block (for e.g. if you're testing every 4 KiB, then test at "address + 0xFFC" rather than at "address"). Even better - test at the start and end of each block.
- Don't use large block sizes - e.g. rather than testing each 1 MiB block, test each 4 KiB block. There's no guarantee that whole MiB are available even though whole MiB are installed. For example, I've seen 80386 systems that remap some of the RAM underneath the ROM to the end of memory, so that with 4 MiB of RAM installed there's 3.25 MiB of RAM from 0x00100000 to 0x0043FFFF/
As you can see, this can be a lot more complicated than it really should have been; mostly because of poor or lacking standards before Phoenix invented "INT 0x15, EAX=0x0000E820" (or before ACPI adopted Pheonix's "INT 0x15, EAX=0x0000E820"?).
The important thing is to do the best you can, so that you can blame the hardware or BIOS if it doesn't work...

Cheers,
Brendan