简体   繁体   中英

segment:offset in a constant

I'm trying to learn assembly. I've written this short real mode function that works as I want to.

getndrives:
        push    bx
        push    es
        push    si
            mov     bx,0040h
            mov     es,bx
            mov     si,0075h
            mov     al,byte [es:si]
        pop     si
        pop     es
        pop     bx
    ret

I would like to have the segment:offset in a constant instead and eliminate the need to push/pop es+si. Something like

biosmem EQU 0040h:0075h
mov al,[biosmem]

above code compiles but does not return the intended result.

x86 doesn't have a way to load from an immediate-constant far-pointer in one instruction. There's a far jmp ptr16:16 that can set cs:ip to a 32-bit immediate, but for loads I don't see any option that doesn't use a segment register.


NASM docs suggest that it is/was typical in 16-bit calling conventions for DS to be call-preserved, but that ES could be considered call-clobbered for exactly the reason you want (in memory models that don't keep everything in a single segment).

So you could consider using a calling convention for this function that allows it to clobber ES . (Or FS or GS if your code only needs to run on 386 or later.)

You can also save instructions by reusing the same register for the segment. You could have used si both times instead of bx , because you're done with bx as soon as you set es .

getndrives:
 ;; return in AL (zero-extended to AX; the upper byte of 0040h happens to be 0)
 ;; clobbers: AH, ES
    mov     ax, 0x0040
    mov     es, ax
    mov     al, [es:0x0475]
    ret

As a bonus, you're loading into AL so it can use the special moffs encoding of mov that skips the ModR/M byte so it's just (ES segment override prefix) + A0 75 04 .

You can of course still push/pop es and/or ax around this if you want, but this is obviously clunkier. And if you're going to save/restore a segment register, @Fifoernik points out it might as well be DS to save a prefix on the mov (unless you have any interrupt handlers that assume DS stays constant).

getndrives:
 ;; return in AL (zero-extended to AX; the upper byte of 0040h happens to be 0)
 ;; clobbers: AH,  or nothing if you uncomment the push/pop of AX
    push    es
    ;push    ax                 ; you might as well use a reg other than AX to simplify this, if you do want to preserve AH, too.  And also for performance on CPUs that don't rename low8 partial regs separately, so mov al,[mem] has a dependency on the pop.
      mov     ax, 0x0040
      mov     es, ax            ; or use DS if you don't need it in any interrupt handler
    ;pop     ax
      mov     al, [es:0x0475]
    pop     es
    ret

If you only care about 186 and newer, push 0040h / pop es would avoid clobbering AH. (8086 doesn't have push imm8/imm16 ).


In your case, you can probably avoid modifying a segment register in the first place . Notice that 0040h:0075h is a linear address of 0475h , which you can access with just an offset from any DS value from 0 to 47h . (In real mode, linear = (segment << 4) + offset , ie left-shift by one hex digit).

With DS=0, you can still access your boot-sector code/data ( according to this PC memory map , loaded at 07C0:0 , aka 0:7C00 ), and anywhere else in the low 64kiB of memory.

I don't actually write 16-bit code, so I'm not sure exactly how you'd tell NASM that you're going to set DS=0 and then have it generate offsets accordingly. But hopefully that's possible.

As a bonus, it saves code size: xor ax,ax is smaller than mov ax, imm16 . But I guess if you were optimizing for code size, you'd push 0 / pop ds . (But 8086 doesn't have push imm , that came later). And I guess if you want stack pointers to be compatible with other pointers, you'd need to mov ss, ax / mov sp, whatever so that's more code.

But anyway, then your function would look like this.

getndrives:
 ;; return in AL
 ;; requires/assumes: DS=0
    mov     al, [0x0475]
    ret

At this point it's silly to make it a function. Make it a macro, or better, or %define BIOS_BDA_ndrives 0x0475 so you can do stuff like add dl, [BIOS_BDA_ndrives] . Or possibly %define BIOS_BDA_ndrives byte [0x0475] to get build-time type checking, eg mov ax, BIOS_BDA_ndrives would then fail to assemble with an operand-size mismatch.

The segment part of an address always uses a segment register. You can use a constant for the offset.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM