Detecting Memory (x86): Difference between revisions

m
Fix lint errors
[unchecked revision][unchecked revision]
mNo edit summary
m (Fix lint errors)
 
(31 intermediate revisions by 13 users not shown)
Line 31:
 
Usage:
<sourcesyntaxhighlight lang="asm">
; Clear carry flag
clc
Line 42:
 
; AX = amount of continuous memory in KB starting from 0.
</syntaxhighlight>
</source>
 
Note: this function is supposed to be always present, and may not modify the carry flag. If an emulator doesn't support it, the carry flag will be set, indicating error.
Line 60:
In reality, this function returns an unsorted list that may contain unused entries and (in rare/dodgy cases) may return overlapping areas.
Each list entry is stored in memory at ES:DI, and DI is <b>not</b> incremented for you. The format of an entry is 2 uint64_t's and a uint32_t in the 20 byte version,
plus one additional uint32_t in the 24 byte ACPI 3.0 version (but nobody has ever seen a 24 byte one).
It is probably best to always store the list entries as 24 byte quantities -- to preserve uint64_t alignments, if nothing else. (Make sure to set
that last uint64_t to 1 before each call, to make your map compatible with ACPI).
Line 117:
It is possible to get a fairly good memory map using Plug 'n Play (PnP) calls. {Need description and code.}
 
====SMBiosSMBIOS====
 
SMBIOS is designed to allow "administrators" to assess hardware upgrade options or maintain a catalogue of what hardware a company current has in use (ie. it provides information for use by humans, rather than for use by software). It may not give reliable results on many computers -- see:
Line 131:
This function doesn't work in real mode. Instead, it's supposed to be called from 32-bit protected mode. It returns the same information as function E801, but uses extended registers (EAX/EBX/ECX/EDX).
 
Should be available on everything made after 1994.
Of course, this function is completely useless without documentation on how to call it.
 
Since this is meant to be used in protected mode, the INT instruction cannot be used to call it. The correct calling procedure is unknown, but it might be the same as for protected mode EISA BIOS functions. Further information is probably buried in a Compaq technical reference manual somewhere.
 
====BIOS Function: INT 0x15, AX = 0xE801====
Line 149 ⟶ 151:
 
Linux Usage:
<sourcesyntaxhighlight lang="asm">
XOR CX, CX
XOR DX, DX
Line 166 ⟶ 168:
; AX = number of contiguous Kb, 1M to 16M
; BX = contiguous 64Kb pages above 16M
</syntaxhighlight>
</source>
 
====BIOS Function: INT 0x15, AX = 0xDA88====
Line 180 ⟶ 182:
 
Usage:
<sourcesyntaxhighlight lang="asm">
CLC ; CF bug workaround
MOV AH, 0x88
Line 188 ⟶ 190:
JE SHORT .ERR
; AX = number of contiguous KB above 1M
</syntaxhighlight>
</source>
 
====BIOS Function: INT 0x15, AH = 0x8A====
Line 235 ⟶ 237:
 
Usage:
<sourcesyntaxhighlight lang="c">
unsigned short total;
unsigned char lowmem, highmem;
Line 246 ⟶ 248:
total = lowmem | highmem << 8;
return total;
</syntaxhighlight>
</source>
 
====E820h====
Line 252 ⟶ 254:
There are a few other BIOS functions that claim to give you memory information. However, they are so widely unsupported that it is impossible to even find machines to test the code on. <b>All</b> current machines support E820 (above). If some user should happen to dig up such a dinosaur of a machine that its BIOS does not support any standard memory detection function -- they will not complain that your modern OS fails to support that machine. Just give an error message.
 
==== Manual Probing ====
{{Warning|This could possibly damage your computer.}}
Use BIOS to get a memory map, or use [[GRUB]] (which calls BIOS for you). Memory probing can have results that are '''unpredictable by nature''' because memory probing is unsupported by vendors.
 
===== Theoretical introduction =====
Use BIOS to get a memory map, or use [[GRUB]] (which calls BIOS for you).
Direct memory probing is only useful for very old systems with buggy and/or non-updateable BIOSes, or maybe a system with modified hardware that does not match the firmware anymore. So if you don't intend to support this sort of computers, you don't need memory probing. Period. Even if you need, you may decide that it's safer to run will less memory than is available.
 
Anyway, don't think that this will save you from the effort of understanding how to call the complicated BIOS APIs. Before launching a probe, you will always need to detect '''if''' the computer where your OS is running really needs it, and '''which''' memory areas really need to be probed (because for other memory areas you should always consider the information provided by the BIOS as authoritative). You also need to take into account the appropriate memory holes and/or memory mapped devices, which may vary from system to system.
When perfectly implemented, directly probing memory may allow you to detect extended 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). The algorithm may or may not take into account potential holes in
system memory or previously detected memory mapped devices, such as frame buffering SVGA cards, etc.
 
When perfectly implemented, directly probing memory may allow you to detect extended memory on systems where the BIOS fails to provide the appropriate support. However, the algorithm will always need to take into account potential holes in system memory or previously detected memory mapped devices, such as frame buffering SVGA cards, etc. Maybe you will want to probe just a specific range of memory that is known to be otherwise undetectable on a specific computer model.
However, the BIOS knows things{{which}} you ignore about your motherboard and PCI devices. Probing memory-mapped PCI devices may have ''unpredictable results''{{what}} and
may theoretically '''damage your system'''{{how}}.
 
However, the BIOS is part of the computer, and may know things (see [[#Practical obstacles to memory probing]]) you ignore about your memory, motherboard and PCI devices. Probing memory-mapped PCI devices may have '''unpredictable results'''. The results are unpredictable by nature because memory probing is unsupported by vendors. The most likely result is crashing you computer, but you can even '''damage your system permanently''', such as clearing firmware chips or setting unsafe device operation parameters. Just remember the [https://en.wikipedia.org/wiki/CIH_(computer_virus) Chernobyl] computer virus.
Note: You will never get an error from trying to read/write memory that does not exist -- this is important to understand: you will not get valid results, but you won't get an error, either.
 
Note: You will never get an error from trying to read/write memory that does not exist -- this is important to understand: you will not get valid results, but you won't get an error, either.
=====Theoretical obstacles to probing=====
 
===== Practical obstacles to memory probing =====
There follows a list of technical difficulties involved in memory probing that may help to implement such an algorithm if you end up having to do it:
* There can be important data structures left in RAM by the BIOS (e.g. the ACPI tables) that you'd trash. Those structures may be anywhere and the only way to know their addresses is to "ask" the BIOS.
* There can be a memory mapped device from 15 MB to 16 MB (typically "VESA local bus" video cards, or older ISA cards that aren't limited to just video).
* There can also be a(n) (extremely rare) "memory hole" at 0x00080000 for some sort of compatibility with ancient cards.
Line 276 ⟶ 280:
* There are also (typically older) motherboards where you can write a value to "nothing" and read the same value back due to bus capacitance; there are motherboards where you write a value to cache and read the same value back from cache even though no RAM is at that address.
* There are (older, mostly 80386) motherboards that remap the RAM underneath the option ROMs and BIOS to the end of RAM. (e.g. with 4 MB of RAM installed you get RAM from 0x00000000 to 0x000A0000 and more RAM from 0x00100000 to 0x00460000, which causes problems if you test each MB of RAM because you get the wrong answer -- either under-counting RAM up to 0x00400000, or over-counting RAM up to 0x00500000).
* There can be important data structures left in RAM by the BIOS (e.g. the ACPI tables) that you'd trash.
* If you write the code properly (ie. to avoid as many of the problems as you can), then it is <i>insanely</i> slow.
* Lastly, testing for RAM (if it actually works) will only tell you where RAM is - it doesn't give you a complete map of the physical address space. You won't know where you can safely put memory mapped PCI devices because you won't know which areas are reserved for chipset things (e.g. SMM, ROMs), etc.
Line 302 ⟶ 305:
 
{{Quotation
| If bit 6 in the flags uint16_t 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<nowiki>):''</nowiki>
}}
 
 
Taking this into account, our example code would look like the following. Note that if you prefer a version that does not require the multiboot.h header downloaded from the link above, there is a version listed in the code examples section of this article.
 
<syntaxhighlight lang="c">
 
 
#include "multiboot.h"
void _main(multiboot_info_t* mbd, uint32_t magic)
{
/* Make sure the magic number matches for memory mapping*/
if(magic != MULTIBOOT_BOOTLOADER_MAGIC) {
panic("invalid magic number!");
}
 
/* Check bit 6 to see if we have a valid memory map */
if(!(mbd->flags >> 6 & 0x1)) {
panic("invalid memory map given by GRUB bootloader");
}
 
/* Loop through the memory map and display the values */
int i;
for(i = 0; i < mbd->mmap_length;
i += sizeof(multiboot_memory_map_t))
{
multiboot_memory_map_t* mmmt =
(multiboot_memory_map_t*) (mbd->mmap_addr + i);
 
printf("Start Addr: %x | Length: %x | Size: %x | Type: %d\n",
mmmt->addr, mmmt->len, mmmt->size, mmmt->type);
 
if(mmmt->type == MULTIBOOT_MEMORY_AVAILABLE) {
/*
* Do something with this memory block!
* BE WARNED that some of memory shown as availiable is actually
* actively being used by the kernel! You'll need to take that
* into account before writing to memory!
*/
}
}
}
</syntaxhighlight>
 
'''WARNING:''' If you downloaded the multiboot header from gnu.org (linked above) you probably got a version which defines the base address and length fields as one 64-bit unsigned integer each, rather than two 32-bit unsigned integers each. [https://forum.osdev.org/viewtopic.php?t=30318 This may cause gcc to pack the structure incorrectly] which can lead to nonsensical values when you try to read it.
* The linked forum post blames GCC for not properly packing the multiboot struct, however, the real error was the printf implementation/usage. When using the type uint64_t, you must specify %lx (instead of %x) so that printf will read all 64-bits as one argument rather than reading the high-32 as one argument and the low-32 as the next argument.
 
Alternatively, you can modify the multiboot header, specifically the multiboot_mmap_entry struct, to the following to get the correct values:
 
<syntaxhighlight lang="c">
struct multiboot_mmap_entry
{
multiboot_uint32_t size;
multiboot_uint32_t addr_low;
multiboot_uint32_t addr_high;
multiboot_uint32_t len_low;
multiboot_uint32_t len_high;
#define MULTIBOOT_MEMORY_AVAILABLE 1
#define MULTIBOOT_MEMORY_RESERVED 2
#define MULTIBOOT_MEMORY_ACPI_RECLAIMABLE 3
#define MULTIBOOT_MEMORY_NVS 4
#define MULTIBOOT_MEMORY_BADRAM 5
multiboot_uint32_t type;
} __attribute__((packed));
typedef struct multiboot_mmap_entry multiboot_memory_map_t;
</syntaxhighlight>
 
Each multiboot mmap entry is stored as the following:
 
:{| {{wikitable}}
|-
! -40
| size
|-
! 04
| base_addr_low
|-
! 48
| base_addr_high
|-
! 812
| length_low
|-
! 1216
| length_high
|-
! 1620
| type
|-
|}
 
 
* '''WARNING:''' If you downloaded the multiboot header from gnu.org, linked above, you probably got a version which defines the base address and length fields as one 64-bit unsigned integer each, rather than two 32-bit unsigned integers each. [https://forum.osdev.org/viewtopic.php?t=30318 This may cause gcc to pack the structure incorrectly] which can lead to nonsensical values when you try to read it.
* "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.
* GRUB simply uses INT 15h, EAX=E820 to get the detailed memory map, and does not verify the "sanity" of the map. It also will not sort the entries, retrieve any available ACPI 3.0 extended uint32_t (with the "ignore this entry" bit), or clean up the table in any other way.
Line 340 ⟶ 410:
Or it might assume that you mean 32M of total <i>usable</i> RAM, going from 0 to 32M + 384K -1. So don't be surprised if you see a "detected memory size"
that does not exactly match your expectations.
 
== What about on UEFI? ==
On UEFI, you have 'BootServices->GetMemoryMap'. This function is similar to E820 and is the only solution on new UEFI machines. Basically, to use, first you call it once to get the size of the memory map. Then you allocate a buffer of that size, and then call again to get the map itself. Watch out, by allocating memory you could increase the size of the memory map. Considering that a new allocation can split a free memory area into two, you should add space for 2 additional memory descriptors. It returns an array of EFI_MEMORY_DESCRIPTORs. They have the following format (taken from GNU EFI):
<syntaxhighlight lang="c">
typedef struct {
UINT32 Type; // EFI_MEMORY_TYPE, Field size is 32 bits followed by 32 bit pad
UINT32 Pad;
EFI_PHYSICAL_ADDRESS PhysicalStart; // Field size is 64 bits
EFI_VIRTUAL_ADDRESS VirtualStart; // Field size is 64 bits
UINT64 NumberOfPages; // Field size is 64 bits
UINT64 Attribute; // Field size is 64 bits
} EFI_MEMORY_DESCRIPTOR;
</syntaxhighlight>
To traverse them, you can use the NEXT_MEMORY_DESCRITOR macro.
 
Memory types are different to the E820 codes. For converting, see [https://github.com/tianocore/edk2/blob/70d5086c3274b1a5b099d642d546581070374e6e/OvmfPkg/Csm/LegacyBiosDxe/LegacyBootSupport.c#L1601 CSM E820 compatibility].
<syntaxhighlight lang="c">
typedef enum {
EfiReservedMemoryType,
EfiLoaderCode,
EfiLoaderData,
EfiBootServicesCode,
EfiBootServicesData,
EfiRuntimeServicesCode,
EfiRuntimeServicesData,
EfiConventionalMemory,
EfiUnusableMemory,
EfiACPIReclaimMemory,
EfiACPIMemoryNVS,
EfiMemoryMappedIO,
EfiMemoryMappedIOPortSpace,
EfiPalCode,
EfiPersistentMemory,
EfiMaxMemoryType
} EFI_MEMORY_TYPE;
</syntaxhighlight>
 
==Code Examples==
Line 347 ⟶ 453:
Declare the 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+sizeof(mmap->size) to the pointer, because mmap->size does not take itself into account and because GRUB treating base_addr_low as offset 0 in the structure. You must also use mmap_length to make sure you don't overshoot the entire buffer.
 
<sourcesyntaxhighlight lang="c">
typedef struct multiboot_memory_map {
unsigned int size;
Line 369 ⟶ 475:
...
}
</syntaxhighlight>
</source>
 
===Getting an E820 Memory Map===
 
<sourcesyntaxhighlight lang="asm">
; use the INT 0x15, eax= 0xE820 BIOS function to get a memory map
; note: initially di is 0, be sure to set it to a value so that the BIOS code will not be overwritten.
Line 425 ⟶ 531:
stc ; "function unsupported" error exit
ret
</syntaxhighlight>
</source>
 
Sample in C (assuming we are in a bootloader environment, real mode, DS and CS = 0000):
<sourcesyntaxhighlight lang="c">
// running in real mode may require:
__asm__(".code16gcc\n");
Line 486 ⟶ 592:
}
}
</syntaxhighlight>
</source>
 
===Getting an UEFI Memory Map===
<syntaxhighlight lang="c">
EFI_STATUS Status;
EFI_MEMORY_DESCRIPTOR *EfiMemoryMap;
UINTN EfiMemoryMapSize;
UINTN EfiMapKey;
UINTN EfiDescriptorSize;
UINT32 EfiDescriptorVersion;
 
//
// Get the EFI memory map.
//
EfiMemoryMapSize = 0;
EfiMemoryMap = NULL;
Status = gBS->GetMemoryMap (
&EfiMemoryMapSize,
EfiMemoryMap,
&EfiMapKey,
&EfiDescriptorSize,
&EfiDescriptorVersion
);
ASSERT (Status == EFI_BUFFER_TOO_SMALL);
 
//
// Use size returned for the AllocatePool.
//
EfiMemoryMap = (EFI_MEMORY_DESCRIPTOR *) AllocatePool (EfiMemoryMapSize + 2 * EfiDescriptorSize);
ASSERT (EfiMemoryMap != NULL);
Status = gBS->GetMemoryMap (
&EfiMemoryMapSize,
EfiMemoryMap,
&EfiMapKey,
&EfiDescriptorSize,
&EfiDescriptorVersion
);
if (EFI_ERROR (Status)) {
FreePool (EfiMemoryMap);
}
 
//
// Get descriptors
//
EFI_MEMORY_DESCRIPTOR *EfiEntry = EfiMemoryMap;
do {
// ... do something with EfiEntry ...
EfiEntry = NEXT_MEMORY_DESCRIPTOR (EfiEntry, EfiDescriptorSize);
} while((UINT8*)EfiEntry < (UINT8*)EfiMemoryMap + EfiMemoryMapSize);
</syntaxhighlight>
 
===Manual Probing in C===
Line 494 ⟶ 649:
* the assembly language manual probing code that follows this example is better
 
<sourcesyntaxhighlight lang="c">
/*
* void count_memory (void)
Line 569 ⟶ 724:
outb(0xA1, irq2);
}
</syntaxhighlight>
</source>
 
===Manual Probing in ASM===
Line 586 ⟶ 741:
* it's better to assume that memory holes are present (and risk skipping some RAM) than to assume that memory holes aren't present (and risk crashing). This means assuming that the area from 0x00F00000 to 0x00FFFFFF can't be used and not probing this area at all (it's possible that some sort of ISA device is in this area, and that any write to this area can cause problems).
 
<sourcesyntaxhighlight lang="asm">
;Probe to see if there's RAM at a certain address
;
Line 643 ⟶ 798:
pop eax
ret
</syntaxhighlight>
</source>
 
Further Notes:
Line 658 ⟶ 813:
 
[[Category:X86]]
[[Category:Physical Memory]]
[[Category:Hardware Detection]]