CLOSE

Retrieving memory information in a bootloader is essential for understanding the layout and availability of system memory. Memory detection allows you to determine where different memory regions are available and which areas should not be overwritten, like reserved memory for system devices or the BIOS.

The different sections of the memory in the x86 architecture is termed into two parts:

  • Low Memory
  • Extended Memory

Low Memory (Conventional Memory)

Low memory refers to the first 1 MB of the memory address space in an x86 system. This is the memory that is directly addressable in real mode.

Extended Memory

Extended memory refers to all memory above the 1 MB boundary in an x86 system. This memory is not directly addressable in real mode but can be accessed in protected mode or through special mechanisms in real mode.

1️⃣ Detecting Low Memory

"Low memory" is the available RAM below 1MB, and usually below 640KB. There are two BIOS functions to get the size of it.

INT 0x12: Detecting Conventional Memory

Out: AX = Number of Kilobytes of conventional memory available in the system.

When you invoke INT 12h, the BIOS returns the size of conventional memory in kilobytes in the AX register.

The INT 0x12 call will return AX = total number of KB. The AX value measures from 0, up to the bottom of the EBDA (of course, you probably shouldn't use the first 0x500 bytes of the space either -- i.e. the IVT or BDA).

    ; Clear carry flag
    clc
 
    ; Switch to the BIOS (= request low memory size)
    ; Call BIOS interrupt 12h to get conventional memory size
    int 0x12

    ; After this, AX will contain the size of conventional memory in KB
    ; For demonstration, let's store the result at a memory location
    mov [conventional_memory_size], ax
 
    ; The carry flag is set if it failed
    jc .Error
 
    ; AX = amount of continuous memory in KB starting from 0.
    ;; Print the value of AX
    
    
conventional_memory_size resw 1 ; Reserve space for the memory size
  • int 0x12: This line invokes BIOS interrupt 12h. After this interrupt call, the AX register contains the size of conventional memory in kilobytes.
  • mov [conventional_memory_size], ax: This line moves the value in AX (which contains the size of conventional memory) to a reserved memory location named conventional_memory_size.

Example in the TheTaaJ:

We have created a function GetLowerMemorySize, which doesn't takes any parameters and just executes the interrupt 0x12 and prints the size of lower memory if success otherwise prints the error string. This function is called from the stage2.asm file. Both code is given below along with the output which i got in QEMU.

stage2/includes/memory.inc:

%ifndef _STAGE_2_MEMORY_INC_
%define _STAGE_2_MEMORY_INC_

BITS 16

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;*********************************
; GetLowerMemorySize
; IN: Nothing
; OUT: Nothing
GetLowerMemorySize:
	pusha
	clc		; Clear the carry flag
	int 0x12	; Interrupt to get the number of low (conventional) memory
	jc .GetLowerMemory_Error
	
	; if carry flag is not set means function successfully returned
	; size of lower memory in AX register
	; AX is having the count in KB
	
	mov si, sLowerMemoryCountSentence	; display lower memory count sentence.
	call PrintString16BIOS
	
	;; Now print the size from the AX in decimal
	call PrintWordNumber		; This ones display the contents of AX in decimal
	call PrintNewline		; \n
	
	popa		; pop all saved registers.

ret	; return to the caller	

; Trap code
.GetLowerMemory_Error:
	mov si, sInt10hFailed		; Error string
	call PrintString16BIOS
	call PrintNewline		; \n
hlt
; ********************************
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Variables

sLowerMemoryCountSentence db 'Lower Memory Size (In KB) : ', 0
sInt10hFailed		db	'Int 0x10 Failed to return lower memory size', 0

%endif

stage2.asm:

	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	;; Detect Size of Lower (Conventional) Memory
	call GetLowerMemorySize
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Output:

image-155.png

You can get the complete code at: GitHub - The-Jat/TheTaaJ at a43cec2062d6eb99a37a1154a854dfb3de4ead93

2️⃣ Detecting Extended Memory

There are variety of other method of getting high memory, however none is much effective as the int 15h, ah = E820. The older method is int 12h which is not reliable.

INT 15h, AH=88h: Detecting Extended Memory:

When you call INT 15h with AH set to 88h, the BIOS returns the amount of extended memory (above 1 MB) in kilobytes in the AX register. This is useful for determining the amount of additional memory available in systems with more than 1 MB of RAM.

Ⅱ Using INT 15h, EAX = E820h to Get the Memory Map

The E820h function is more advanced compared to the older INT 12h and INT 15h, AH = 88h calls. It returns information about all memory regions, including reserved areas, usable memory, and memory-mapped I/O regions.

Memory Map Entry Structure:

Each memory region is described by a structure that includes the base address, length, and type of the memory region. The structure typically looks like this:

struct e820_entry {
    uint64_t base_addr; // Base address of the memory region
    uint64_t length;    // Length of the memory region in bytes
    uint32_t type;      // Type of the memory region
    uint32_t acpi_attributes;	// Only vaid if ECX = 24 (0x18)
};

What is int 0x15?

The int 0x15 BIOS interrupt is a general-purpose interrupt used for various system services. It provides functions related to:

  • System configuration
  • Extended memory
  • Advanced Power Management (APM)
  • System keyboard intercepts
  • Block moves in memory

Steps to Detect High Memory (Extended Memory):

1 Prepare the Registers and Call BIOS Interrupt:
  • Set EAX to 0xE820 to specify the function that you want to retrieve the memory map.
  • Set EBX to 0 for the first call. For subsequent calls, use the value of EBX returned by the previous call.
  • Set ECX to the size of the buffer you are providing for the memory map entry, which is 24 the size of each memory descriptor in bytes.
  • Set EDX to the magic number 0x534D4150 ('SMAP' in ASCII) to identify the call. This is a signature to ensure that the BIOS returns valid data.
  • Set ES:DI to point to a buffer that will hold the returned memory map entry.
2 Parse the Returned Data:
  • After the interrupt call, if CF (Carry Flag) is clear, EAX will still be 0x534D4150 and the buffer pointed to by ES:DI will contain the memory map entry. The memory map entry is a 24-byte structure for each region of memory.
  • The returned structure typically looks like this:
struct e820_entry {
    uint64_t base_address;
    uint64_t length;
    uint32_t type;
    uint32_t acpi_attributes;  // Only valid if ECX = 24 (0x18)
};
OffsetSizeDescription
0x008 bytesBase Address (64-bit)
0x088 bytesLength (64-bit)
0x104 bytesType (32-bit)
0x144 bytesReserved
  • Memory Map Entry Structure:
    • Base Address (64-bit)
      • Offset: 0
      • Size: 8 bytes
      • Description: The starting address of the memory range.
    • Length (64-bit)
      • Offset: 8
      • Size: 8 bytes
      • Description: The length (size) of the memory range in bytes.
    • Type (32-bit)
      • Offset: 16
      • Size: 4 bytes
      • Description: The type of memory in this range. The type indicates how this memory range should be used basically the purpose of it.
      • Only regions of type 1 (usable) should be considered for loading an operating system or programs.
    • ACPI 3.0 Extended Attributes (32-bit)
      • Offset: 20
      • Size: 4 bytes
      • Description: Extended attributes defined in the ACPI 3.0 specification. (Optional and may not be provided by all BIOS implementations)
- `base_address` is the starting address of the memory region.
- `length` is the length of the memory region.
- `type` defines the type of the memory region:
    - 1: Usable (RAM)
    - 2: Reserved
    - 3: ACPI Reclaimable
    - 4: ACPI NVS
    - 5: Bad Memory
3 Repeat the Call:
  • Continue calling the interrupt with the updated EBX until EBX returns 0, indicating that there are no more entries.

Here is an example of how this might be done in assembly language:

; Setup registers for e820 call
mov eax, 0xE820          ; Function number
mov ebx, 0               ; Continuation value, 0 for first call
mov ecx, 24              ; Size of the structure
mov edx, 0x534D4150      ; SMAP signature
mov edi, buffer          ; Pointer to the buffer to store the result
int 0x15                 ; BIOS interrupt

jc  no_more_entries      ; If carry flag is set, no more entries

; If we get here, we've successfully retrieved a memory map entry
; The buffer now contains the e820 entry structure

; Process the buffer as needed
;...

; Prepare for the next call
mov eax, 0xE820
mov ecx, 20
mov edx, 0x534D4150
mov edi, buffer
; EBX was updated by the BIOS to continue where it left off
int 0x15

; Repeat until no more entries
...

no_more_entries:
; Handle end of memory map

Note:

  • Ensure that the buffer pointed to by EDI is large enough to hold the structure. The size can be 20 bytes or 24 bytes if the optional ACPI 3.0 attributes are included.

Below is the Complete Example Code:

In the code given below we have used the int 0x15 and its function E801 to get the memory map and stored it at location MEMLOCATION_MEMORY_MAP which is 0x9000 defined in defines.inc.

defines.inc:

%define MEMLOCATION_MEMORY_MAP	0x9000

memory.inc:

SetupMemory:
; ********************************
; SetupMemory
; ********************************
SetupMemory:
	; Save state 
	pusha	; saves the state of all general-purpose registers on the stack.


	; Print the reading memory map statement:
	mov si, sReadingMemoryMapSentence
	call PrintString16BIOS
	call PrintNewline


	; Step 1, try to get memory map
	mov 	edi, MEMLOCATION_MEMORY_MAP     ; Set EDI to the memory location
											; where the memory map will be
											; stored.
	mov 	dword [dataStructureObj + DataStructure.MemMapAddr], edi ; Save this address in the data structure
	                                                                 ; `dataStructureObj` at offset DataStructure.MemMapAddr

	;; Print the Memory Map address on screen in hex with its statement.
	mov 	si, sMemMapAddrSentence
	call	PrintString16BIOS
	mov	word dx, [dataStructureObj + DataStructure.MemMapAddr]
	call	PrintWordHex
	call	PrintNewline

	;; Memory Map function to retreive the memory map entries using BIOS interrupt 0x15, function E820
	call 	LoadMemoryMap

	; Save entry count in structure
	mov 	dword [dataStructureObj + DataStructure.MemMapLength], ebp

	;; Print the Memory map count
	mov	word ax, [dataStructureObj + DataStructure.MemMapLength]	
	mov	si, sMemMapLengthSentence
	call	PrintString16BIOS
	call	PrintWordNumber
	call	PrintNewline

	; Restore
	popa
ret
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LoadMemoryMap:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; ********************************
; LoadMemoryMap
; IN:
;	- ES:EDI: Destination Buffer
; OUT:
; 	- EBX: 0 -> Success, 1 -> Err
; ********************************
LoadMemoryMap:
	; Save buffer
	push 	edi

	; Clear out space for memory map (1 Page)
	xor 	eax, eax	; Zero out EAX, common and efficient way to clear a register
	mov 	ecx, 1024	; counter
	rep 	stosd		; `stosd` =  string instruction that uses `EDI` register as a
				; destination pointer. It writes the value of `EAX`
				; to the memory location pointed to by `EDI`
				; `rep` = repeat the `stosd` instruction `ECX` times (1024)
				; times
				; The whole block code zeroes out 1024 double words (each 4
				; bytes) of memory start from the address to by `EDI`
				; It effectively fills a 4096 bytes (1024*4) block of memory
				; with zeroes.

	; Restore
	pop 	edi

	; Setup INT 0x15
	mov eax, 0xE820		; Function number  = E820
	xor ebx, ebx		; Zero out ebx
	xor ebp, ebp		; Zero out base pointer
	mov ecx, 24		; Size of the structure (including ACPI attributes)
	mov edx, BIOS_MEMMAP_SIGNATURE	; SMAP signature

	; Ready, make sure we force a valid ACPI 3.X Entry
	mov [es:di + 20], dword 1

	int 0x15	; Read Memory Map
	jc .NoMemoryMap	; unsuccessful call if carry flag is set.

	; Restore EDX for safety (EAX contains signature on success)
	mov 	edx, BIOS_MEMMAP_SIGNATURE
	cmp 	eax, edx		; Compare the signature
	jne 	.NoMemoryMap		; if mismatch unsuccessful

	; If ebx is 0, only 1 entry, we cant use that for anything
	test 	ebx, ebx	; Performs bitwise AND operation between EBX
				; and then set the processor's flag based on the result.
				; This is common idiom in assembly language to check
				; is a register is zero or not.
	je 	.NoMemoryMap	; if zero, means no memory map
	
	; Proceed if memory map is valid
	jmp 	.MediasRes

	; Each call returns one memory map entry at a time.
	; Loop for subsequent int 0x15 calls
	.cLoop:
		; Query time
		mov 	eax, 0xE820	; Function number for memory map query
		mov 	ecx, 24		; Size of memory map structure
		mov 	[es:di + 20], dword 1	; Set ACPI attribute to 1
		int 	0x15			; Invoke BIOS interrupt 0x15
		jc 		.Done		; Jump to Done if carry flag is set (end of memory map)
		mov 	edx, BIOS_MEMMAP_SIGNATURE

	.MediasRes:
		; Parse Entry
		test 	ecx, ecx		; Test if ecx is zero
		je 		.SkipEntry	; Jump to SkipEntry if ecx is zero
		cmp 	cl, 20			; Compare lower byte of ecx with 20
		jbe 	.Acpi2			; Jump to Acpi2 if cl <= 20
		test 	byte [es:di + 20], 1	; Test if ACPI attribute bit 0 is set
		je 		.SkipEntry	; Jump to Skip Entry if ACPI attribute bit 0 is set

	; ACPI 2.0 Entry
	.Acpi2:
		; Acpi2 Entry

		; Get lower dword of memory length
		mov 	ecx, [es:di + 8]
		; Check if lowe dword is not zero
		test 	ecx, ecx	; Test if ecx is zero
		jne 	.GoodEntry	; Jump to GoodEntry if ecx is not zero

		; Get higher dword of memory length
		mov 	ecx, [es:di + 12]	; Get higher dword of memory length
		test 	ecx, ecx		; Test if ecx is zero
		je 		.SkipEntry

	.GoodEntry:
		; Increase entry count & move to next
		inc 	ebp		; Increment ebp (entry count)
		add 	edi, 0x20	; Move edi to the next memory map entry (each entry is 32 bytes)

	.SkipEntry:
		; If ebx resets to 0, list is complete
		test 	ebx, ebx		; Test if ebx is zero
		jne 		.cLoop		; Jump to cLoop if ebx is not zero
		jmp 		.Done		; Jump to done if ebx is zero

	.NoMemoryMap:
		mov	si, sNoMemoryMapSentence
		call	PrintString16BIOS
		call	PrintNewline
		mov 	ebx, 1			; set ebx to 1  (indicating failure)
		mov 	ebp, 0			; Clear ebp (entry count)
		ret				; Return

	.Done:
		mov 	ebx, 0			; Set ebx to 0 (indicating success)
ret

stage2.asm:

call SetupMemory	; calls the setup memory
Output:
image-156.png

Ⅲ Retrieve Memory Size using Int 15h, AX = E801h:

INT 15h, AX=E801h

This function provides a way to determine the amount of installed memory beyond the 16MB boundary, which is useful for systems with larger memory configurations.

To call this function, you need to set AX to 0xE801 and then invoke the interrupt 0x15. The BIOS will return the memory information in the registers AX, BX, CX, and DX.

Registers on Return

  • AX: Number of contiguous 1 KB blocks of memory between 1 MB and 16 MB.
  • BX: Number of contiguous 1 KB blocks of memory above 16 MB.
  • CX: If the value in AX is less than or equal to 15, then CX contains the same value as AX. Otherwise, it contains the number of contiguous 64 KB blocks of memory between 16 MB and 4 GB.
  • DX: If the value in AX is less than or equal to 15, then DX contains the same value as BX. Otherwise, it contains the number of contiguous 64 KB blocks of memory above 4 GB.

Input:

  • AX = 0xE801

Output:

  • If successful:
    • CX = Number of contiguous kilobytes of memory between 1MB and 16MB.
    • DX = Number of 64KB blocks of memory above 16MB.
    • BX = Number of contiguous kilobytes of memory below 1MB (optional and usually not used).
    • AX = Number of kilobytes of memory above 16MB (alternative to DX).

Complete Example:

We will have a function named as GetMemorySize which invokes the interrupt 0x15 with AH = E801, and returns the amount of memory between 1MB and 16MB in EAX and amount of memory above 16MB mark in EBX.

We will call this function from the SetupMemory function in memory.inc file just after we got the memory map.

; ********************************
; GetMemorySize
; OUT:
; 	- EAX: KB between 1 mb and 16 mb (0 on err)
; 	- EBX: Num of 64 Kb blocks above 16 mb (0 on err)
; ********************************
GetMemorySize:
	; Clear registers
	xor 	ecx, ecx
	xor 	edx, edx
	
	; Setup INT 0x15
	mov 	eax, 0xE801		; EAX = 0xE801 (function code for getting memory size)
	int 	0x15

			; call BIOS interrupt 0x15


    ; Check for error by examining the Carry Flag (CF)
	jc 		.Error		; If CF is set, jump to .Error handling

	; Check if the function is unsupported (AH = 0x86)
	cmp 	ah, 0x86	; Compare AH with 0x86
	je 		.Error		; If equal, jump to error handling

	; Check if the command is invalid (AH = 0x80)
	cmp 	ah, 0x80	; Compare AH with 0x80
	je 		.Error		; If equal, jump to error handling

	; It is possible that bios uses ECX/EDX instead
	test 	ecx, ecx	; Test if ECX is zero
	je 		.UseEAX		; If ECX is zero, jump to use EAX values

	; If ECX is not zero, BIOS returned values in ECX and EDX
	mov 	eax, ecx	; EAX = ECX (KB between 1 MB and 16 MB)
	mov 	ebx, edx	; EBX = EDX (number of 64 KB blocks above 16 MB)

	.UseEAX:
		; If ECX was zero, use the values already in EAX and EBX
    	ret                     ; Return with EAX and EBX set

	.Error:
		; In case of an error, set EAX and EBX to 0
		mov 	eax, 0		; EAX = 0 (error indication)
		mov 	ebx, 0		; EBX = 0 (error indication)
	ret