4
\$\begingroup\$

The time has come in the development of the initialization process of my OS to get details about memory. I could have done this a lot simpler as both systems I use, have 4 gigs, but on every system that I'll use my software, I want detailed reporting. This code is somewhat fashioned after the example detecting memory at wiki.osdev.org, minus some functionality and a lot less bloat.

My development environment is an HP Thinkcenter, dual boot Ubuntu 16.04 LT and DOS 6.22 with the ability to boot from USB. The code has been tested with BOCH's, DOS .com file and sector 0 of USB. The only part that hasn't been tested is if bits 31 - 2E aren't zero. Then again, I doubt seriously there will every be a single entry with a minimum length of 2 gig.

NOTE: DosBox yields blank screen under Linux

I've included the source for NASM and if anyone has some real hardware it could be tested on with a screenshot of actual results, that would be much appreciated.

The prolog and epilog for each is functionally the same, but with a few differences peculiar to each paradigm.

Boot

I use BOCH's and DEBUG without symbols, so these page alignments make it easier to remember where the buffer is and for memory dumps.

7C00  FA                cli
7C01  BDF0FF            mov     bp, -16  Reserve space for BP
7C04  89EC              mov     sp, bp
7C06  83EC30            sub     sp, 48
7C09  FB                sti

Not going to assume BIOS passes all segment registers pointing to the same place.

7C0A  8CD0              mov     ax, ss
7C0C  8ED8              mov     ds, ax
7C0E  8EC0              mov     es, ax

This boot sector was assembled without an ORG directive, therefore everything is an offset from zero. CS must be set appropriately.

7C10  EA1500C007        jmp     0x7c0:0x15

Clears the video page determined by DISP_PAGE, Sets the initial output to center of second line. This will be bumped by 1 later, so values actually start on line 3.

7C15  E8CF00            call    ClrScr
7C18  C746009001        mov     [bp], 0x190
7C1D  89E7              mov     di, sp         DI = Pntr to E820 data

7C1F  E82E00            call    GetMap

In this case as DISP_PAGE is defined as 0, 7C22 -> 7C2A and 7C31 -> 7C38 are redundant, but in the event value is changed in line 10 of source, this needs to be here.

7C22  B80005            mov     ax, SAP << 16
7C25  50                push    ax
7C26  B000              mov     al, DISP_PAGE
7C28  CD10              int     VIDEO

Wait for reponse from operator and convert to uppercase
7C2A  B80000            mov     ax, 0x0
7C2D  CD16              int     KEYBOARD
7C2F  245F              and     al, 0x5f
7C31  67870424          xchg    ax,[esp]
7C35  CD10              int     VIDEO

'Y' or 'y' will reboot, anything else hangs
7C37  58                pop     ax
7C38  3C59              cmp     al, 'Y'
7C3A  7502              jnz     7c3e
7C3C  CD19              int     REBOOT
7C3E  F4                hlt
7C3F  EBFE              jmp     $

To calculate breakpoints, add this value to desired address in application body.

7C50  GetMap

     253  NULLS to padd object code to boot signature

    7DFE  55 AA

DOS

100  BCF0FF            mov      sp, -16
103  C81E0000          enter    30, 0
107  E8BD00            call     ClrScr
10A  C746009001        mov      [bp],0x190
10F  89E7              mov      di, sp          DI = FFC0

111  E81C00            call     GetMap

Unlike the original version of DEBUG that came bundled with DOS 6.22, I use a version that is 32 bit capable, but if one displays and debugs on same page, it just becomes a big mess, so output is directed to page 1 @ B9???. To see the results, I switch to that page, wait for any kind of response and then revert to page 0 again.

114  B80005            mov      ax, SAP << 16   AH = 5 Set active page
117  50                push     ax
118  B001              mov      al, DISP_PAGE
11A  CD10              int      VIDEO
11C  B80000            mov      ax, GETKEY
11F  CD16              int      KEYBOARD
121  58                pop      ax
122  CD10              int      VIDEO
124  C9                leave
125  CD20              int      TERMINATE

To calculate breakpoints, add this value to desired address in application body.

130  GetMap

This code is common to both BOOT & DOS. To determine absolute address add value denoted by GetMap in previous examples.

Initialize registers required by INT 15

 0  6631DB            xor   ebx, ebx
 3  6689D9            mov   ecx, ebx
 6  B120              mov   cl, 32
 8  66BA50414D53      mov   edx, 534d4150   'SMAP'
 E  B820E8            mov   ax, 0xe820

Loop until BL = 0, CF = 1 or EAX <> EDX when BL = 1

11  50                push  ax
12  6652              push  edx
14  C6451401          mov   [di+20], 1      ACPI 3.0
18  CD15              int   SYS_SERVICE
1A  7211              jc    0x2d
1C  80FB01            cmp   bl, 1
1F  720C              jc    0x2D
21  7707              ja    0x2A            Not first iteration
23  66673B0424        cmp   eax,[esp]   EAX = 'SMAP'
28  7503              jnz   0x2D
2A  E80600            call  0x33

2D  665A              pop   edx
2F  58                pop   ax
30  73DF              jnc   0x11        Read another entry
32  C3                ret

Being in real mode (16 bit) were limited to 32 bit data, so these 64 bit values passed by BIOS need to be digested in 32 bit chunks.

33  51                push  cx          Preserve continuation
34  89FE              mov   si, di
36  8B4602            mov   ax,[bp+2]       Video segment
39  06                push  es
3A  8EC0              mov   es, ax
3C  57                push  di  
; At the beginning of each conversion, bump to center of next line

3D  8B7E00            mov   di,[bp]
40  81C7A000          add   di,160
44  897E00            mov   [bp], di

BIOS colors correspond directly to map types which are displayed on center column

47  8A6410            mov   ah,[si+16]
4A  B0FE              mov   al,0xfe
4C  AB                stosw
4D  83EF06            sub   di, 6       Back to left of center
50  57                push  di

The loop between 51 <-> 77 needs to be executed twice. Once for base address and then again for length of entry. This next instruction facilitates that without the need for excessive jumps or calls.

NOTE: This is the only instruction that relies on ORG 100H as it is absolutely unlike everything else that is relative.

51  689D01            push 0x19D

One thing I've never liked is leading zeros unless this is a specific reason for it, so this part only displays what needs to be.

54  66AD              lodsd                 Bits 31 - 0
56  E81F00            call  0x78
59  66AD              lodsd         Bits 63 - 32
5B  6609C0            or    eax, eax

At this point, if EAX = 0 we don't need to padd with CL zero's
5E  7501              jnz   0x61
60  C3                ret

CL = positions left to fill with '0' to complete all 8. Should have maybe tested for zero, but REP STOSW won't do anything if so

61  6650              push  eax
63  B83007            mov   ax,0x730    Attr in AH = White
66  FD                std           Need to do in reverse
67  F3AB              rep   stosw
69  6658              pop   eax             Restore map value

Because we don't care about CL and DF is already set, we can simply
drop into procedure that actually starts @ 0x78
6B  EB0E              jmp   0x7B

This is where lenth of map segment is displayed
6D  5F                pop   di
6E  83C728            add   di, 40
71  E8E0FF            call  0x54

Restore and read next map entry
74  5F                pop   di
75  07                pop   es
76  59                pop   cx
77  C3                ret

This routine converts binary value in EAX to hexidecimal notation (upper case)

78  B108              mov   cl, 8       Max digits
7A  FD                std           Set DF to decreemnt

Save value of EAX and then convert AL to ASCII equivalent
7B  6650              push  eax
7D  240F              and   al, 0xf
7F  3C0A              cmp   al, 0xa
81  7202              jc    0x85
83  0407              add   al, 0x7     10-15 become A-F
85  0430              add   al, 0x30
87  B407              mov   ah, 0x7     Display in White
89  AB                stosw
8A  6658              pop   eax
8C  FEC9              dec   cl              Bump digits displayed
8E  66C1E804          shr   eax, 4
92  75E7              jnz   0x7b        While EAX
94  FC                cld           Reset DF to inc
95  F8                clc   So primary loop won't bail.
96  C3                ret

Clearing the screen could have been done by setting video mode, but for some reason on my test machine, this takes probably 2.5 seconds, so I implemented this instead.

97  06                push  es
98  B800B8            mov   ax, 0xb800
9B  80C401            add   ah, DISP_PAGE
9E  8EC0              mov   es, ax
A0  894602            mov   [bp+2], ax
A3  57                push  di
A4  31FF              xor   di, di
A6  B82007            mov   ax, 0x720 White on Black with spaces
A9  51                push  cx
AA  B9D007            mov   cx, 0x7d0 80 x 25 words
AD  F3AB              rep   stosw
AF  59                pop   cx
B0  5F                pop   di
B1  07                pop   es
B2  C3                ret

Assemble as follows for

Boot

nasm E820.asm -D_BOOT_ -oBoot.bin

DOS

nasm E820.asm -oE820.com

Haven't quite figured out why sometimes source tabulates properly and sometimes it doesn't, but anyway I sure if you copy this into an editor of your choice, it will look a lot better

E820.asm

       SAP  equ  5      ; Function to set active page
    GETKEY  equ  0      ; Read keyboard input

     VIDEO  equ  16         ; BIOS Video services
      KEYBOARD  equ  22
      BOOT  equ  25
   SYS_SERVICE  equ  21

%ifdef _BOOT_
    DISP_PAGE   equ  0      ; Choose page 0 for Bochs

    cli         ; Disable interrupts
    mov bp, -16     ; Scatch area for BP
    mov sp, bp
    sub sp, 48      ; Space for map entries (page aligned)
    sti

    ; BIOS should probably of set these, but just in case

    mov ax, ss
    mov ds, ax
    mov es, ax

    ; NOTE: Because original is at zero so quirky BIOS's won't break my code.

    jmp 0x7c0:Start
%else
  DISP_PAGE equ  1      ; Choose page 1 for DOS & DEBUB

    org 256     ; For DOS com files
    mov sp, -16     ; 16 Byte scratch area for BP
    enter   30, 0
%endif

 Start: call    ClrScr      ; Clear selected display page
    mov word [bp], 400  ; Ouput begins right justifed Line=3 Col=40 
    mov di, sp      ; Point to E820 entry buffer

    call    GetMap

    ; This is for the benefit of BOCHS or maybe even QEMU and DOS so result
    ; can be viewed before re-booting or alternatively returning to DEBUG

    mov ax, 0x500   ; Select page 0
    push    ax
    mov al, DISP_PAGE   ; Display results page
    int VIDEO
    mov ax, GETKEY
    int KEYBOARD    ; Wait for reponse from operator

%ifdef _BOOT_
    and al, 0x5f    ; 
    xchg    ax, [esp]
%else
    pop ax  
%endif

    int VIDEO       ; Set to default video page 0

%ifdef _BOOT_
    pop ax
    cmp al, 'Y'
    jnz $ + 4
    int BOOT        ; Let's do it again
    hlt
    jmp $       ; Spin  
%else
    leave           ; Kill procedure frame
    int 32      ; Terminate DOS application
%endif

    ; This is here so the object code for BOOT version versus DOS version
    ; is at the same offset from the beginning of executable.

    align   16

GetMap: xor ebx, ebx    ; Continuation starts @ 0
    mov ecx, ebx
    mov cl, 32      ; Anything greater than 20
    mov edx, 'PAMS'
    mov ax, 0xe820

    ; Inifinte loop unless
    ;     A: RBX = 0
    ;     B: EAX <> EDX
    ;     C: Service call returns CF = 1

  .L0:  push    ax
    push    edx
    mov byte [di+20], 1 ; This has something to do with ACPI 3
    int SYS_SERVICE
    jc  .done       ; Call return CY=1
    cmp bl, 1
    jb  .done       ; BL = 0
    ja  .done - 3   ; Not first time
    cmp eax, [esp]  ; Compare values
    jnz .done
    call    DMap        ; Convert and display two 64 bit values

 .done: pop edx
    pop ax
    jnc .L0     ; This might break if EAX <> EDX
    ret

  DMap: push    cx      ; Save continue value for INT 15H
    mov si, di      ; Pointer to 4 64 bit values
    mov ax, [bp+2]  ; Get video segment
    push    es
    mov es, ax
    push    di      ; Preserve address to map data
    mov di, [bp]    ; Next position to write to in video
    add di, 160     ; Bump to next line
    mov [bp], di    ; and save
    mov ah, [si+16] ; Get entry type
    mov al, 254     ; Small block
    stosw           ; Set entry type indicator
    sub di, 6       ; Bump back to left side
    push    di
    push    .C0     ; Re-entrant

   ; First invocation display base address of entry, next time length.

   .J0: lodsd           ; Convert and display low order 32 bits
    call    WVal
    lodsd
    or  eax, eax
    jnz $ + 3
    ret

    push    eax
    mov ax, 0x730
    std
    rep stosw       ; Write CL leading zeros
    pop eax
    jmp WVal.J0     ; Drop into because DF is already set

   .C0: pop di
    add di, 40      ; Right most position of 
    call    .J0     ; Display length on entry
    pop di
    pop es
    pop cx
    ret

    ; Converts and display 32-bit value in EAX in reverse order

  WVal: mov cl, 8
    std

   .J0: push    eax
    and al, 15
    cmp al, 10
    jb  $ + 4
    add al, 7
    add al, '0'
    mov ah, 7
    stosw
    pop eax
    dec cl
    shr eax, 4
    jnz .J0

    ; In the event leading zero's need to be displayed, the number is returned
    ; in CL

    cld         ; Re-set direction flag
    clc         ; So primary loop won't break
    ret

ClrScr: push es
    mov ax, 0xb800
    add ah, DISP_PAGE
    mov es, ax
    mov [bp+2], ax
    push    di
    xor di, di
    mov ax, 0x720   ; White on Black / spaces
    push    cx
    mov cx, 80*25
    rep stosw
    pop cx
    pop di
    pop es
    ret

%ifdef _BOOT_
    times   510 - ($-$$) db 0
    dw  0xAA55
%endif 
\$\endgroup\$
3
\$\begingroup\$

NOTE: DosBox yields blank screen under Linux

Same here for DOSBox under Windows and also for DOS 6.20 by itself! DOSBox (and for that matter the NTVDM also) doesn't support the 0000E820h BIOS function at all. Read on to find out why a true DOS 6.20 fails ...

Observations and optimizations

; This is here so the object code for BOOT version versus DOS version
; is at the same offset from the beginning of executable.
align   16

To calculate breakpoints, add this value to desired address in application body. 7C50 GetMap

To calculate breakpoints, add this value to desired address in application body. 130 GetMap

This align directive can not do what you state. Up to this point the BOOT version has 65 bytes that get padded till the address is at 80, while the DOS version has 38 bytes that get padded till the address is 48. You apparantly knew this looking at the last 2 digits in the numbers in the other comments:

This boot sector was assembled without an ORG directive, therefore everything is an offset from zero. CS must be set appropriately.

7C10  EA1500C007        jmp     0x7c0:0x15

Only the push .C0 instruction imposes that you set the CS segment register this way. Without that push the far jump could have been omitted.
You've taken good precautions with not trusting how BIOS sets the segment registers.
You should take the same precaution with regard to the direction flag DF. cld

SAP          equ   5      ; Function to set active page
GETKEY       equ   0      ; Read keyboard input
VIDEO        equ  16      ; BIOS Video services
KEYBOARD     equ  22
BOOT         equ  25
SYS_SERVICE  equ  21

When I first read the above equates, I felt a bit lost. I've never come across any api documentation that did not express all of these numbers in hexadecimal and always using precisely 2 digits. Although what you wrote is numerically correct, it would be advisable to go with the flow and write:

SAP          equ  05h      ; Function to set active page
GETKEY       equ  00h      ; Read keyboard input
VIDEO        equ  10h      ; BIOS Video services
KEYBOARD     equ  16h
BOOT         equ  19h
SYS_SERVICE  equ  15h

I bet you more people will recognize these numbers without hesitation as being correct.

cli         ; Disable interrupts
mov bp, -16     ; Scatch area for BP
mov sp, bp
sub sp, 48      ; Space for map entries (page aligned)
sti

Using cli/sti around this modification of SP is not necessary. If you would also modify the SS segment register then it would make sense as it would guarantee a consistent stackpointer.
Instead of first copying BP to SP and then subtracting 48 from SP, you can write this in a single instruction using lea.

mov bp, -16     ; Scratch area for BP
lea sp, [bp-48] ; Space for map entries (page aligned)

This has just become 40% shorter.
The comment mentions "page aligned". Could it be you meant "paragraph aligned" ?

mov sp, -16     ; 16 Byte scratch area for BP
enter   30, 0

Having a lexical level of 0, this enter 30, 0 instruction is the equivalent of

push    bp
mov     bp, sp
sub     sp, 30

The problem with all of this is that your program is using the word at [bp] for 2 different things at a time. You yourself will store here the offset in video memory AND ALSO you allow BIOS to use this word by asking for a SMAP-record of max. 32 bytes.
Furthermore there's also this difference between the BOOT version and the DOS version. Why does the BOOT version get 48 bytes for map entries as opposed to DOS who gets only 30 bytes? Could it be you forgot the hex-prefix on 0x30 which is equal to 48?
I think you can use the self same code for both versions.

mov bp, -16     ; Scratch area for BP
lea sp, [bp-48] ; Space for map entries (paragraph aligned)
mov word [bp], 400  ; Ouput begins right justifed Line=3 Col=40 

Correct instruction, misleading comment. Video offset 400 points to the 41st column on the 3rd row.
So (40,2) if expressed 0-based (BIOS,...) and (41,3) if expressed 1-based (BASIC,...).

mov ax, GETKEY
int KEYBOARD    ; Wait for reponse from operator

This is a tricky error! The function number should have gone in the AH register but because GETKEY is equal to 0 the code worked just fine. You did waste 1 extra byte though.

and al, 0x5f    ; 

The intention here is to UCase the character inputted by the user. Why then do you additionally drop bit 7 (8th bit) ? In doing so you allow the user to not only use 'Y' and 'y' to reboot but also '┘' and '¨'. Just pointing out this inconsistency with the comment:

'Y' or 'y' will reboot, anything else hangs.


hlt
jmp $       ; Spin  

If you're going to use hlt then best go back to it, if for some reason execution should resume.

hlt
jmp $-1       ; Spin  
leave           ; Kill procedure frame
int 32      ; Terminate DOS application

This leave instruction (mov sp, bp pop bp) is wasted right before exiting to DOS. Apart from the CS segment register the OS doesn't expect anything else from the application to be able to terminate it. Moreover the value that leave pops back into the BP register is corrupted because of the reason I gave discussing enter 30, 0.

mov  ax, [bp+2]  ; Get video segment
push es
mov  es, ax
push di
mov  di, [bp]    ; Next position to write to in video
add  di, 160     ; Bump to next line
mov  [bp], di    ; and save

No need to use an intermediate step clobbering AX.

push    es
mov     es, [bp+2]  ; Get video segment

Shorter if adding directly to memory.

push    di
add     word [bp], 160     ; Bump to next line in video
mov     di, [bp]

Saving even more through combining both. Notice we have an offset in the low word and a segment in the high word? Ideal situation to use the les instruction.

push    es
push    di
add     word [bp], 160     ; Bump to next line in video
les     di, [bp]

Total bytes saved 7 (from 17 bytes down to 10 bytes)

push    di
push    .C0     ; Re-entrant
; First invocation display base address of entry, next time length.
.J0: lodsd           ; Convert and display low order 32 bits
call    WVal
lodsd
or  eax, eax
jnz $ + 3
ret
push    eax
mov ax, 0x730
std
rep stosw       ; Write CL leading zeros
pop eax
jmp WVal.J0     ; Drop into because DF is already set
.C0: pop di
add di, 40      ; Right most position of 
call    .J0     ; Display length on entry

Although this methode of using an embedded subroutine (I wouldn't say re-entrant) works OK (I verified it), I find it is too clever for my taste. A solution that converts a 64-bit number held in 2 32-bit registers is much simpler, not to mention shorter. It's easy to write using the shrd instruction.

clc         ; So primary loop won't break
ret

You've accidently placed this clc before the wrong ret.

call    .J0     ; Display length on entry
pop di
pop es
pop cx
            <-- This is where the CLC belongs!
ret

Now the funny thing is that an explicit clc isn't really needed if addresses and lengths stay below 4GB because the or eax, eax instruction will be the last CF modifying instruction on the execution path (CF=0).


The troubles with GetMap

There's a lot to be said here. So much that I had to re-write the code to get it working.

  • The function number for this QuerySystemAddressMap is the 32-bit number 0000E820h that goes in EAX. I gave your optimizing for code size AX approach a chance on several of my computers and they all returned an error. (CF=1 and AH=86h)
  • The 32-bit continuation value that goes in EBX is a number who's actual value (when non-zero) is completely at the discretion of BIOS. It could be a very small monotonously incrementing counter like you are expecting but this is by no means an obligation for BIOS.
  • That same continuation value (when zero) does not represent an error condition of some kind. Your code however treats it that way. It's only BIOS saying: "Don't come back for more records!"
    Because of this misconception your routine presently does not display the last record!
  • The 4-character signature in EDX solely exists to guard against false positives on older BIOSes. An unaware BIOS might return CF=0 when an application calls this non-existing 0000E820h function. Therefore BIOS must acknowledge the signature on every invocation. Trying to verify this acknowledgement only once just complicates matters.
  • By keeping the constants in EAX, ECX, and EDX outside of the loop, you've given yourself a hard time because you had to preserve these values and dealing with

    jnc .L0 ; This might break if EAX <> EDX
    

    wasn't so obvious anymore, that is to say, you didn't solve it.

New code:

GetMap:
    xor     ebx, ebx    ; Continuation starts at 0
.a: xor     ecx, ecx
    mov     cl, 24
    mov     edx, 'PAMS'
    mov     eax, 0000E820h
    int     15h
    jc      .done
    cmp     eax, edx    ; EDX='PAMS'
    jne     .done
    call    DMap        ; Convert and display two 64 bit values
    test    ebx, ebx
    jnz     .a          ; More records to retrieve
.done:
    ret

I applied all of the above to your program and the size of the executable came down from 230 bytes (minimal corrections) to 182 bytes (optimizations applied). I ran it on an Acer Aspire computer with a Phoenix BIOS and 2GB RAM.

This is a screenshot

Colors legend:

blue    free memory
green   reserved memory
cyan    ACPI reclaimable memory
red     ACPI non-volatile memory
magenta bad memory
\$\endgroup\$
  • \$\begingroup\$ Interesting how the ISA hole gobbles up so much memory. On my Lenovo Thinkpad desktop, it starts @ CFFB0000 almost 1 gig. I've just finished my bootloader and going to spend a few days documenting, testing and tweaking. Memory map has been a bit of an issue, mostly due to lack of adequate information. \$\endgroup\$ – Shift_Left Jul 16 '17 at 14:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.