What and why ...

Determining how much memory you have is one of the first things that most people implement in their kernel. In the "old" days of operating systems this was very easy as few people had more than 64mb of memory.

Why did I mention 64mb of memory? Because the CMOS can only hold values up to 99mb. So what are you going to do if you encounter machines with 128mb? There are some functions in the BIOS for memory handling. The first set of calls supported by all BIOS only returns what is in the CMOS, thus making it irrelevant. There is some more advanced calls but are not kuse are directly probing memory, and using your motherboard chipset registers to determine memory. The major drawback with the later methods is you must know what kind of chipset the user has on his motherboard....

Something to note, not that you may need to know, is that on old machines (maybe even new ones), it is possible to have less than 640kb base memory and still have extended memory beyond the 1mb mark. But in today's machines where most people have single 64mb DIMM's, you wont come across this.

A map of low (conventional) memory

There are regions that are used for the system and that the BIOS will never report to you because they're assuming well-known standards. Quoting The Workings of: x86-16/32 RealMode Addressing by Perica Senjak (minor corrections by BrendanTrotter):

        start        end      size  region/exception       description

Low Memory (the first MiB)
     00000000 - 000003FF       400  RAM                    Real-Mode IVT (Interrupt Vector Table)
     00000400 - 000004FF       100  RAM                    BDA (BIOS data area)
     00000500 - 0009FBFF   ? 9F700  RAM/free for use       Conventional memory (<= 9F700 Byte)
     00007C00 - 00007DFF       200  RAM                    Operating System BootSector
     0009FC00 - 0009FFFF       400  RAM                    EBDA (Extended BIOS data area)
     000A0000 - 000FFFFF     60000  various                ROM Area (384 KiB)

    Standard usage of the ROM Area:
     000A0000 - 000BFFFF     20000  video RAM               VGA Mem (128 KiB)
     000A0000 - 000AFFFF     10000  video RAM                VGA framebuffer (64 KiB)
     000B0000 - 000B7FFF      8000  video RAM                text monochrom  (32 KiB)
     000B8000 - 000BFFFF      8000  video RAM                text color      (32 KiB)
     000C0000 - 000C7FFF      8000  ROM                     Video BIOS* (32 KiB is typical size)
     000F0000 - 000FFFFF     10000  ROM                     Motherboard BIOS* (64 KiB is typical size)

High Memory (everything after the first MiB)
     00100000 - FEBFFFFF  FEB00000  RAM?/free for use?     Extended memory
     01000000 - 010FFFFF    100000  ?                       ISA 15-16MB (only with ISA bus?)
     FEC00000 - FFFFFFFF   1400000  various                PnP NVRAM?, LAPIC, ...

Counting RAM using the BIOS

You can determine RAM size with the BIOS via two different calls.

The first call is built in nearly every BIOS, the later call is only contained within newer BIOS's (from Ralf Browns Interrupt List:)

 --------B-1588-------------------------------
 INT 15 - SYSTEM - GET EXTENDED MEMORY SIZE (286+)
   AH = 88h
   Return: CF clear if successful
     AX = number of contiguous KB starting at absolute address 100000h
     CF set on error
   AH = status
     80h invalid command (PC,PCjr)
     86h unsupported function (XT,PS30)

Notes: DOS TSRs which wish to allocate extended memory for themselves often hook this call, and return a reduced memory size. They are then free to use the memory between the new and old sizes at will. If your OS boots from DOS (e.g. lin-loader for linux), this may become more relevant.

The standard BIOS only returns memory between 1MB and 16MB; use AH=C7h for memory beyond 16MB not all BIOSes correctly return the carry flag, making this call unreliable unless one first checks whether it is supported through a mechanism other than calling the function and testing CF

In these times though, these functions are 'poor' to say the least. A newer function that does the same thing is:

INT 15h
AX = E801h

Return:
  CF clear if successful
  AX = extended memory between 1M and 16M, in K (max 3C00h = 15MB)
  BX = extended memory above 16M, in 64K blocks
  CX = configured memory 1M to 16M, in K
  DX = configured memory above 16M, in 64K blocks CF set on error

This function has been around since about 1994, so all systems from after then up to now should have this function.

Note: For optimum compatibility with all systems you should use the functions in the order: E820h, E881h, E801h and resort to 88h/C7h if everything else fails.

 SeeAlso:AH=87h,AH=8Ah"Phoenix",AH=C7h,AX=DA88h,AX=E801h,AX=E820h

 --------b-15E820-----------------------------
 INT 15 - newer BIOSes - GET SYSTEM MEMORY MAP
   AX = E820h
   EAX = 0000E820h
   EDX = 534D4150h ('SMAP')
   EBX = continuation value or 00000000h to start at beginning of map
   ECX = size of buffer for result, in bytes (should be <= 20 bytes)
   ES:DI -> buffer for result (see #00560)
   Return: CF clear if successful
     EAX = 534D4150h ('SMAP')
     ES:DI buffer filled
     EBX = next offset from which to copy or 00000000h if all done
     ECX = actual length returned in bytes
     CF set on error
       AH = error code (86h) (see #00475 at INT 15/AH=80h)

Notes:

  • this function is now supported by most newer BIOSes
  • a maximum of 20 bytes will be transferred at one time, even if ECX is higher; some BIOSes ignore the value of ECX on entry, and always copy 20 bytes
  • some BIOSes expect the high word of EAX to be clear on entry, I.e. EAX=0000E820h

If this function is not supported, an application should fall back to AX=E802h, AX=E801h, and then AH= 88h the BIOS is permitted to return a nonzero continuation value in EBX and indicate that the end of the list has already been reached by returning with CF set on the next iteration this function will return base memory and ISA/PCI memory contiguous with base memory as normal memory ranges; it will indicate chipset-defined address holes which are not in use and motherboard memory-mapped devices, and all occurrences of the system BIOS as reserved standard PC address ranges will not be reported

 SeeAlso:AH=C7h,AX=E801h"Phoenix",AX=E881h,MEM xxxxh:xxx0h"ACPI"

 Format of Phoenix BIOS system memory map address range descriptor:
 Offset Size Description (Table 00559)
  00h QWORD base address
  08h QWORD length in bytes
  10h DWORD type of address range (see #00560)

 (Table 00560)
 Values for System Memory Map address type:
  01h memory, available to OS
  02h reserved, not available (e.g. system ROM, memory-mapped device)
  03h ACPI Reclaim Memory (useable by OS after reading ACPI tables)
  04h ACPI NVS Memory (OS is required to save this memory between NVS
         sessions)
  other not defined yet -- treat as Reserved
  SeeAlso: #00559

ACPI 3.0 Notes:

  • Version 3.0 of the ACPI standard extends "int 15h, AX=E820"
  • A type of 05h has been defined for "memory in which errors have been detected"
  • A 32 bit set of flags ("Extended Attibutes") has been appended to the end of the 20 byte structure, making it 24 bytes.
  • Bit 0 of the Extended Attributes indicates if the entire entry should be ignored (if the bit is clear). This is going to be a huge compatibility problem because most current OSs won't read this bit and won't ignore the entry.
  • Bit 1 of the Extended Attributes indicates if the entry is non-volatile (if the bit is set) or not. The standard states that "Memory reported as non-volatile may require characterization to determine its suitability for use as conventional RAM.".
  • The remaining 30 bits of the Extended Attributes are undefined.

BIOS reports xxx, is this normal ?

The 'standard' gap between 0xA0000 and 0xFFFFF will never be reported by BIOS.

ToDo does anybody have examples of output to show ? It seems that many people on the FAQ find GRUB or BIOS memory maps confusing, for instance because they appear to have zero-sized zero-typed slots. -- see this thread to find out...

Asking GRUB the amount of RAM

GRUB, or any bootloader implementing The Multiboot Specification provides a convenient way of detecting the amount of RAM your machine has. Rather than re-invent the wheel, you can ride on the hard work that others have done by utilizing the multiboot_info structure. When GRUB runs, it loads this structure into memory and leaves the address of this structure in the EBX register.

To utilize this structure, first include the file multiboot.h in your kernel's main file. Then, make sure that when you load your _main function from your assembly loader, you push EBX onto the stack. BareBones has this already done for you.

The key for memory detection lies in the multiboot_info struct. To get access to it, you've already pushed it onto the stack...define your start function as such:

   _main (multiboot_info_t* mbd, unsigned int magic) {...}

Now you may just check mbd->flags to see that bit 0 is set, and then you can safely refer to mbd->mem_lower for conventional memory (e.g. physical addresses ranging between 0 and 640KB) and mbd->mem_upper for high memory (e.g. from 1MB). Both are given in "real" kilobytes, i.e. blocks of 1024 bytes each.

If this is still not yet enough for you, you may check bit 6 of mbd->flags and use mbd->mmap_addr to access the BIOS-provided memory map. Quoting specifications,

If bit 6 in the flags word is set, then the mmap_* fields are valid, and indicate the address and length of a buffer containing a memory map of the machine provided by the BIOS. mmap_addr is the address, and mmap_length is the total size of the buffer. The buffer consists of one or more of the following size/structure pairs (size is really used for skipping to the next pair):

             +-------------------+
     -4      | size              |
             +-------------------+
     0       | base_addr_low     |
     4       | base_addr_high    |
     8       | length_low        |
     12      | length_high       |
     16      | type              |
             +-------------------+

where size is the size of the associated structure in bytes, which can be greater than the minimum of 20 bytes. base_addr_low is the lower 32 bits of the starting address, and base_addr_high is the upper 32 bits, for a total of a 64-bit starting address. length_low is the lower 32 bits of the size of the memory region in bytes, and length_high is the upper 32 bits, for a total of a 64-bit length. type is the variety of address range represented, where a value of 1 indicates available RAM, and all other values currently indicated a reserved area.

So in order to use the GRUB memory map you declare an appropriate structure, get the pointer to the first instance, grab whatever address and length information you want, and finally skip to the next memory map instance by adding size+4 to the pointer, tacking on the 4 to account for GRUB treating base_addr_low as offset 0 in the structure. You must also use mmap_length to make sure we don't overshoot the entire buffer.

typedef struct multiboot_memory_map {
unsigned int size;
unsigned int base_addr_low,base_addr_high;
//You can also use: unsigned long long int base_addr; if supported.
unsigned int length_low,length_high;
//You can also use: unsigned long long int length; if supported.
unsigned int type;
} multiboot_memory_map_t;

int main(multiboot_info* mbt,unsigned int magic) {
...
multiboot_memory_map_t* mmap = mbt->mmap_addr;
while(mmap < mbt->mmap_addr + mbt->mmap_length) {
 ...
 mmap = (multiboot_memory_map_t*) ( (unsigned int)mmap + mmap->size + sizeof(unsigned int) );
 };
...
}

Counting RAM by direct probing

WE DISCOURAGE YOU FROM DIRECTLY PROBING MEMORY

Use BIOS to get a memory map, or use GRUB.

When perfectly implemented, directly probing memory may allow you to detect the amount of memory even on systems where the BIOS fails to provide the appropriate support (or without even worrying about whether your BIOS can do it or not). Depending on how its coded, may or may not take into account holes in system memory (15/16mb mark ala OS/2) or memory mapped devices like frame buffering SVGA cards, etc.

However, your BIOS knows things you ignore about your motherboard and PCI devices. Probing memory-mapped PCI devices may have unpredictable results and possibly damage your system, so once again we discourage its use.

That being said, here comes an example of how it should work (note the interrupt disable and the cache invalidation using InlineAssembly to keep memory consistent :)

/*
 * void count_memory (void)
 *
 * probes memory above 1mb
 *
 * last mod : 05sep98 - stuart george
 *            08dec98 - ""     ""
 *            21feb99 - removed dummy calls
 *
 */
void count_memory(void)
{
        register ULONG *mem;
        ULONG   mem_count, a;
        USHORT  memkb;
        UCHAR   irq1, irq2;
        ULONG   cr0;

        /* save IRQ's */
        irq1=inb(0x21);
        irq2=inb(0xA1);

        /* kill all irq's */
        outb(0x21, 0xFF);
        outb(0xA1, 0xFF);

        mem_count=0;
        memkb=0;

        // store a copy of CR0
        __asm__ __volatile("movl %%cr0, %%eax":"=a"(cr0))::"eax");

        // invalidate the cache
        // write-back and invalidate the cache
        __asm__ __volatile__ ("wbinvd");

        // plug cr0 with just PE/CD/NW
        // cache disable(486+), no-writeback(486+), 32bit mode(386+)
        __asm__ __volatile__("movl %%eax, %%cr0", ::
                             "a" (cr0 | 0x00000001 | 0x40000000 | 0x20000000) : "eax");

        do
        {
                memkb++;
                mem_count+=1024*1024;
                mem=(ULONG*)mem_count;

                a=*mem;

                *mem=0x55AA55AA;

                // the empty asm calls tell gcc not to rely on whats in its registers
                // as saved variables (this gets us around GCC optimisations)
                asm("":::"memory");
                if(*mem!=0x55AA55AA)
                        mem_count=0;
                else
                {
                        *mem=0xAA55AA55;
                        asm("":::"memory");
                        if(*mem!=0xAA55AA55)
                                mem_count=0;
                }

                asm("":::"memory");
                *mem=a;
        }while(memkb<4096 && mem_count!=0);

        __asm__ __volatile__("movl %%eax, %%cr0", :: "a" (cr0) : "eax");

        mem_end=memkb<<20;
        mem=(ULONG*)0x413;
        bse_end=((*mem)&0xFFFF)<<6;

        outb(0x21, irq1);
        outb(0xA1, irq2);
}

Related Threads
  • Grub memory map, featuring real examples of GRUB/BIOS reported memory map.

Categories: HowTo, HardWareMemory, UsingBios