4
\$\begingroup\$

No matter the ultimate goal of any legacy X86 system, startup or prologue, should take into consideration the fundamentals. In this case, the intention before dropping into protected or long modes, the user has a means by which to explore the system and have a comprehensive initial screen displaying essential details.

As I don't really expect this code to be implemented by anyone else, the intent is to be informative and detailed enough to be critiqued. This is why the actual address where the code will be executed and opcodes are including in listing.

To facilitate this informative initial screen and for the most part, just for curiosity's sake, the 8 general purpose and 6 segment registers are preserved as passed by BIOS:

7C00  60                pushaw
7C01  06                push    es
7C02  1E                push    ds
7C03  0E                push    cs
7C04  16                push    ss
7C05  0FA0              push    fs
7C07  0FA8              push    gs

There is a lot to be said for backward compatibility, but as I want to populate conventional space (A0000 - 500) with as many even 512 byte sectors (1277) as possible, the boot sector is not really in a convenient spot. There are three significant features this next bit addresses:

  1. Extends boot loader beyond 1 sector.
  2. Initializes CS to a known value.
  3. Moves code to lowest possible position in memory.
7C09  B8 5000           mov     ax, 0x50
7C0C  8EC0              mov     es, ax
7C0E  31DB              xor     bx, bx        ; ES:BX = Destination
7C10  B9 0100           mov     cx, 1         ; Re-read sector 0
7C13  B8 0402           mov     ax, 0x204     ; 
7C16  CD13              int     DISKIO
7C18  7303              jnc     7c1d
7C1A  F4                hlt
7C1B  EBFE              jmp     $
7C1D  EA 6000 5000      jmp     0x50:0x60     ; Long jump, setting CS

Initially, I haven't implemented an error trap as if there wasn't a problem reading the first sector, then the likelihood of there being one in the next three is negligible.

The next thing is probably one of the most important, as the system needs a reasonably large space for stack and temporary data. My coding style is to utilize procedure frames as much as possible and they can be pretty large at times.

 560  FA                cli                 ; Disable interrupts
 561  CD12              int     0x12        ; Get # of 1k blocks
 563  B1 40             mov     cl, 64      ; 1k Blocks to reserve
 565  29C8              sub     ax, cx
 567  C1E0 06           shl     ax, 6       ; AX = Bottom of stack segment
 56A  8EC0              mov     es, ax

Without implementing something that has too much overhead, filling the 64K stack frame with -1 will allow the stack to be probed to see how deep it has been penetrated if necessary.

 56C  31FF              xor     di, di
 56E  C1E1 09           shl     cx, 9       ; 40 << 9 = 512 * 64 = 32768
 571  83C8FF            or      ax, -1
 574  F3AB              rep     stosw       ; Fill frame with FFFFH

Before we can set SS:SP, those 14 values saved right at the start need to be moved from the existing frame to a new one. There is also going to be a 160-byte scratch area reserved at the top of the frame that will be addressed by BP:

 576  BD 60FF           mov     bp, 0xff60  ; Base of scratch area
 579  89EF              mov     di, bp
 57B  83EF 1C           sub     di, 0x1c    ; Space for 14 words 
 57E  89FB              mov     bx, di
 580  89E6              mov     si, sp
 582  16                push    ss
 583  1F                pop     ds
 584  B1 07             mov     cl, 7
 586  F366A5            rep     movsd

Lastly, set new SS:SP:

 589  89DC              mov     sp, bx
 58B  06                push    es
 58C  17                pop     ss
 58D  FB                sti

Notice that some procedures are page aligned. This is not done for any sort of optimization, but I can make small changes in a routine with affecting the one below and as I don't use symbols in BOCH's even addresses are just a little easier to remember.

At this point, anything the BIOS has passed on the stack is gone, including the return address into it. I've never come across anything indicating this to be of any relevance but would need to be taken into account if so.


Anyone interested in experimenting with this can PM me and I'll zip up a tarball with the complete project.

\$\endgroup\$
  • 1
    \$\begingroup\$ There is a hazard here: The stack set up by the ROM-BIOS might overlap the area at 00500h--01000h which would make your code corrupt part of itself. And as in the answer by Sep Roland you should set ss first then in the very next instruction set sp. If you want to depend on a 386+ processor you can use lss sp instead. \$\endgroup\$ – ecm Dec 9 at 15:16
2
\$\begingroup\$
7C09  B8 5000           mov     ax, 0x50

I find it confusing to see how the byte dump is presented. I imagine that you separated the numbers yourself to make the immediate operands stand out, but looking at 5000 more resembles 5000h than the actual 0050h that it needs to represent.

7C1A  F4                hlt
7C1B  EBFE              jmp     $

If you are going to use hlt then why not jump back to it in case execution gets resumed?

7C1A  F4                hlt
7C1B  EBFE              jmp     $-1

You load your boot program at linear address 00500h. That's fine but stay aware that BIOS has its PrintScreenStatus flag here. That's why so many Operating Systems start at linear address 00600h.

7C10  B9 0100           mov     cx, 1         ; Re-read sector 0
7C13  B8 0402           mov     ax, 0x204     ; 
7C16  CD13              int     DISKIO

I would like to point out 2 things here:

  • You know that sector numbering is 1-based (hence mov cx, 1), then perhaps the comment here should not be talking about "sector 0".
  • You don't setup the drive number in DL and indeed you don't need to since BIOS handed you this value from the start, but can you be sure about the head number in DH ?
560  FA                cli                 ; Disable interrupts
561  CD12              int     0x12        ; Get # of 1k blocks
563  B1 40             mov     cl, 64      ; 1k Blocks to reserve
565  29C8              sub     ax, cx
567  C1E0 06           shl     ax, 6       ; AX = Bottom of stack segment
56A  8EC0              mov     es, ax
56C  31FF              xor     di, di
56E  C1E1 09           shl     cx, 9       ; 40 << 9 = 512 * 64 = 32768
571  83C8FF            or      ax, -1
574  F3AB              rep     stosw       ; Fill frame with FFFFH

You would better move the cli instruction below the int 0x12. I don't believe there's too much guarantee that api calls keep interrupts disabled!

I get it that you're trying to write compact code, but it's a bit back-firing here. In mov cl, 64 you only write the low half of the CX register because you cleverly know that the CH part is still empty from the code before. What you forget is that the single instruction sub ax, 64 only requires 3 bytes whereas your pair of instructions mov cl, 64 sub ax, cx requires one byte more.

Because by now you're committed to using what's already in CX, you write a somewhat obfuscating shl cx, 9 where a simple mov cx, 32768 would have been so much clearer.

579  89EF              mov     di, bp
57B  83EF 1C           sub     di, 0x1c    ; Space for 14 words 
57E  89FB              mov     bx, di
580  89E6              mov     si, sp
582  16                push    ss
583  1F                pop     ds
584  B1 07             mov     cl, 7
586  F366A5            rep     movsd
589  89DC              mov     sp, bx
58B  06                push    es
58C  17                pop     ss
58D  FB                sti

This part can be optimized in several ways:

  • Replacing the 2 instructions on top by a single lea di, [bp-28] will save 2 bytes.
  • Instead of using an extra register BX, you can setup SP via lea sp, [di-28]. Again 1 byte shorter.
  • There's no point in transferring dwords here. Stick with words and drop the Operand Size Prefix. Another byte saved.
  • Instead of setting the DS segment register to a value that hereafter will most probably never get used again, you could use a segment override on the string primitive operation and doing so shaves off another byte.
  • Although this code does run with interrupts disabled, modifying SP directly below setting SS is the preferred way.

With all the above applied, your code then becomes:

pushaw
push    es
push    ds
push    cs
push    ss
push    fs
push    gs
mov     ax, 0x50
mov     es, ax
xor     bx, bx        ; ES:BX = Destination
xor     dh, dh        ; Head 0
mov     cx, 1         ; Cylinder 0, sector 1
mov     ax, 0x0204    ; 4 sectors
int     DISKIO
jnc     7c1d
hlt
jmp     $-1
jmp     0x50:0x60     ; Long jump, setting CS
----------------------
int     0x12          ; Get # of 1k blocks
cli                   ; Disable interrupts
sub     ax, 64        ; 1k Blocks to reserve
shl     ax, 6         ; AX = Bottom of stack segment
mov     es, ax
xor     di, di
mov     cx, 32768
mov     ax, -1
rep     stosw         ; Fill frame with FFFFH
mov     bp, 0xFF60    ; Base of scratch area
lea     di, [bp-28]   ; Space for 14 words 
mov     si, sp
mov     cl, 14        ;CH=0
rep     movs word ptr [di],[ss:si]
push    es
pop     ss
lea     sp, [di-28]
sti
\$\endgroup\$
  • \$\begingroup\$ Loading @ 600H and reading 5 sectors instead of 4, fills buffer to 1000H which is the objective. xor dh, dh assumes BIOS has set this value, which isn't a good idea either. Sector number is confusing at best as most applications adopt zero indexing but CHS doesn't. I believe LBA uses zero indexing if I remember correctly. My method of documenting is experimental at best, but yes, this was a time I tried to separate opcode from data, but I think I'll leave well enough alone and just leave what ndisasm emits. \$\endgroup\$ – Shift_Left Jul 2 '17 at 18:05
  • 1
    \$\begingroup\$ Good points about obfuscation, especially where there isn't any advantage in code size. I'll probably leave mov cl,64 as I know the previous mov cx,1 has already cleared CH. I will admit though, I have gotten myself into trouble as if I move code, then that dependancy has been broken. \$\endgroup\$ – Shift_Left Jul 2 '17 at 18:11
  • \$\begingroup\$ What is the encoding for movs word ptr [di],[ss:si]. I can't find anything in any documentation anywhere that would lead me to think MOVS of any kind can be segment overridden much less take parameters. \$\endgroup\$ – Shift_Left Jul 4 '17 at 22:17
  • 3
    \$\begingroup\$ NASM doesn't know about this syntax. For simplicity reasons the designer has chosen that when dealing with string primitives, you put the segment override prefix before the mnemonic like inss movsw. \$\endgroup\$ – Sep Roland Jul 9 '17 at 13:13
0
\$\begingroup\$

Sep Roland's recommendations


Although I not entirely convinced the print screen handler that lives just above BDA is of any consequence to me, from 500H -> 1000H is not evenly divisible by 512, so moving to 600H and reading one more sector satisfies my initial intention of loading as many sectors as possible to 1000:0.

As DL is set to boot device, logic dictates that reading this sector is probably one of the last things BIOS did, so it would stand to reason DH is also set appropriately. Assumptions, however, can and usually do cause a lot of headaches, so for the sake to two bytes, being guaranteed makes sense.

7c09  B86000            mov ax,0x60
7c0C  8EC0              mov es,ax
7c0E  31DB              xor bx,bx
7c10  B600              mov dh,0x0
7c12  B90100            mov cx,0x1
7c15  B80502            mov ax,0x205
7c18  CD13              int 0x13
7c1A  7303              jnc 0x7c1f
7c1C  F4                hlt
7c1D  EBFD              jmp 0x7c1c
7c1F  EA60006000        jmp 0x60:0x60

This represents an 18% saving in space versus the original version, but although writing words instead of dwords helps to that end, it must be noted that it has added a significant amount of time. REP STOSW takes roughly 15 cycles, so this adds 240k cycles to loop and on my computer @ 2.8 gig = 101 micro sec.

NOTE: Calculations are not precise

660  CD12              int  0x12
662  83E840            sub  ax,byte +0x40
665  C1E006            shl  ax,byte 0x6
668  8EC0              mov  es,ax
66A  31FF              xor  di,di
66C  B90080            mov  cx,0x8000
66F  83C8FF            or   ax,byte -0x1
672  F3AB              rep  stosw
674  BD60FF            mov  bp,0xff60
677  8D7EE4            lea  di,[bp-0x1c]
67A  89E6              mov  si,sp
67C  B10E              mov  cl,0xe
67E  F3A5              rep  movsw
680  FA                cli
681  06                push es
682  17                pop  ss
683  8D66E4            lea  sp,[bp-0x1c]
686  FB                sti
\$\endgroup\$
  • \$\begingroup\$ I see that you've skipped the setting of DS altogether! This will only work if the default BIOS setting was DS=SS. For NASM write rep ss movsw. \$\endgroup\$ – Sep Roland Jul 9 '17 at 13:20
  • \$\begingroup\$ @SepRoland Coincidentally, that seemingly minor error did not rear its ugly head until the code was tested on real hardware. Boch's, DosBox under Ubuntu 16.04, DOS 6.22 on fixed media and USB are the four methods used in code development. Wasn't until booting from USB, this became an issue. I read for quite a bit trying to figure out how to segment override MOVS?. Even the intel manual wasn't much help, but then again, I could have missed something there too. \$\endgroup\$ – Shift_Left Jul 9 '17 at 16:10
  • \$\begingroup\$ Is it then NASM that you use for developping this? \$\endgroup\$ – Sep Roland Jul 9 '17 at 16:51
  • 1
    \$\begingroup\$ @SepRoland Yes it is. For there to be a one to one correlation, or near so, of source and object (disassembly with ndisasm), higher level constructs such as macros are avoided. There are a couple of definitions passed via command line, wich facilitates conditional assembly and functionality. \$\endgroup\$ – Shift_Left Jul 9 '17 at 19:29

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.