I wanted to make a few examples of what I mentioned in a comment. That is, to create an MZ .EXE header or to manually implement relocations in a flat format binary, all using only NASM's multi-section -f bin
format.
To save on work, I actually created a single example with multiple build options to select what layout to use exactly.
Some macros used in this example are from my 8086 macro collection found at https://hg.pushbx.org/ecm/lmacros/ and all other files are in https://pushbx.org/ecm/test/20230702/
This is the main sources, test.asm
:
; Public Domain
%include "lmacros3.mac"
numdef DOSEXIT, 0
numdef DOSENTRY, 0
numdef LARGEFILL, 0
numdef RELOC, 1
numdef BASESEGMENT, 0
numdef RELOCEXE, 0
cpu 8086
%if _RELOCEXE
%assign ORIGIN 0
org ORIGIN
addsection HEADER, start=0
header_start:
db "MZ" ; exeSignature
FILESIZE equ fromparas(header_size_p + BEHINDSEGMENT)
dw FILESIZE % 512 ; exeExtraBytes
dw (FILESIZE + 511) / 512 ; exePages
dw relocationtable.amount ; exeRelocItems
dw header_size_p ; exeHeaderSize
dw paras(512) ; exeMinAlloc
dw paras(512) ; exeMaxAlloc
dw +BEHINDSEGMENT ; exeInitSS
dw 512 ; exeInitSP
dw 0 ; exeChecksum
dw 0, +0 ; exeInitCSIP
dw relocationtable ; exeRelocTable
addsection FIRST, vstart=0 align=16 follows=HEADER
%elif _DOSENTRY
%assign ORIGIN 256
org ORIGIN
addsection FIRST, start=ORIGIN
%else
%assign ORIGIN 0
org ORIGIN
addsection FIRST, start=ORIGIN
%endif
first_start:
addsection SECOND, vstart=0 align=16 follows=FIRST
second_start:
addsection THIRD, vstart=0 align=16 follows=SECOND
third_start:
first_size equ first_end - first_start
endarea first, 1
second_size equ second_end - second_start
endarea second, 1
third_size equ third_end - third_start
endarea third, 1
%if _RELOCEXE
header_size equ header_end - header_start
endarea header, 1
%endif
FIRSTSEGMENT equ _BASESEGMENT + 0
SECONDSEGMENT equ _BASESEGMENT + paras(ORIGIN) + first_size_p
THIRDSEGMENT equ _BASESEGMENT + paras(ORIGIN) + first_size_p + second_size_p
BEHINDSEGMENT equ _BASESEGMENT + paras(ORIGIN) + first_size_p + second_size_p + third_size_p
%define RELOCATIONFROMFIRST ""
%define RELOCATIONFROMSECOND ""
%define RELOCATIONFROMTHIRD ""
%imacro relocation 0-1.nolist -2
%%reloc equ $ + %1
%ifidn _CURRENT_SECTION, FIRST
%xdefine RELOCATIONFROMFIRST RELOCATIONFROMFIRST, %%reloc, FIRSTSEGMENT
%elifidn _CURRENT_SECTION, SECOND
%xdefine RELOCATIONFROMSECOND RELOCATIONFROMSECOND, %%reloc, SECONDSEGMENT
%elifidn _CURRENT_SECTION, THIRD
%xdefine RELOCATIONFROMTHIRD RELOCATIONFROMTHIRD, %%reloc, THIRDSEGMENT
%else
%error Unknown section for relocation
%endif
%endmacro
usesection FIRST
start:
mov dx, cs
mov ds, dx
%if !_RELOCEXE && _RELOC
mov si, relocationtable
mov cx, relocationtable.amount
jcxz .noreloc
@@:
lodsw
xchg bx, ax
lodsw
add ax, dx
mov es, ax
add word [es:bx], dx
loop @B
.noreloc:
%endif
displayfirst:
mov si, firstmsg
mov bx, 7
mov ah, 0Eh
db __TEST_IMM16 ; (skip int 10h)
@@:
int 10h
lodsb
test al, al
jnz @B
call SECONDSEGMENT:secondentry
relocation
exit:
%if _DOSEXIT
mov ax, 4C00h
int 21h
%else
xor ax, ax
int 16h
int 19h
%endif
bouncetothird:
jmp THIRDSEGMENT:bounced
relocation
firstmsg: asciz "Hello from first!",13,10
usesection SECOND
secondmsg: asciz "Hello from second!",13,10
secondentry:
displaysecond:
mov si, secondmsg
mov bx, 7
mov ah, 0Eh
db __TEST_IMM16 ; (skip int 10h)
@@:
int 10h
cs lodsb
test al, al
jnz @B
jmp THIRDSEGMENT:thirdentry
relocation
usesection THIRD
align 2, db 0
indirect_to_bounce:
dw bouncetothird
relocation 0
dw FIRSTSEGMENT
thirdmsg: asciz "Hello from third!",13,10
thirdentry:
jmp far [cs:indirect_to_bounce]
bounced:
displaythird:
mov si, thirdmsg
mov bx, 7
mov ah, 0Eh
db __TEST_IMM16 ; (skip int 10h)
@@:
int 10h
cs lodsb
test al, al
jnz @B
retf
%ifn _RELOCEXE
usesection FIRST
%else
usesection HEADER
%endif
%if _RELOCEXE || _RELOC
align 4, db 0
relocationtable:
.:
dw RELOCATIONFROMFIRST
dw RELOCATIONFROMSECOND
dw RELOCATIONFROMTHIRD
.end:
.amount: equ (.end - .) / 4
%endif
%if _RELOCEXE
align 16
header_end:
%endif
%if _LARGEFILL
usesection SECOND
_fill fromkib(64), 0CCh, second_start
%endif
usesection FIRST
align 16
first_end:
usesection SECOND
align 16
second_end:
usesection THIRD
align 16
third_end:
How does it work?
- NASM allows to create far jumps and calls with a hardcoded immediate address, in the format
jmp SEGMENT:OFFSET
(no size keyword, two colon-separated immediate numbers).
- The
relocation
macro defaults to creating an equate for $ - 2
(current output address minus 2), which points into the segment immediate of a preceding instruction. This reference is used to make Self-Modifying Code.
- The macro can be used as
relocation 0
to create a relocation table entry for the next word.
- The relocation entries are emitted into a table. For simplicity this table is here always in the same format as used by the MZ .EXE header.
- The segment values (
FIRSTSEGMENT
and so on) are defined using a base segment plus a displacement, in paragraphs, to the particular segment in our program image. The displacements are calculated from the label deltas that give the length of each segment in the program image.
- The base segment can be nonzero to support loading at a fixed address.
Here are the commands I used to create three different builds of this program:
nasm -I ~/proj/lmacros/ test.asm -o test.com -l testcom.lst -D_DOSENTRY -D_DOSEXIT && dosemu -K "$PWD" -E "ldebug test.com" -dumb -td -kt
Build a flat format .COM file with DOS entry (origin = 256) and DOS termination call. (All I/O is done using the ROM-BIOS interfaces, unconditionally.)
nasm -I ~/proj/lmacros/ test.asm -o test.exe -l testexe.lst -D_DOSENTRY -D_DOSEXIT -D_RELOCEXE -D_LARGEFILL && dosemu -K "$PWD" -E "ldebug test.exe" -dumb -td -kt
Build an MZ .EXE file placing and using the relocation table within the MZ .EXE header. Still use DOS termination call. For fun, fill the SECOND segment to 64 KiB. This requires an MZ .EXE file under DOS, or alternatively what I call a .BIG file. (.BIG files are flat format files similar to .COM files but may exceed 64 KiB and the initial stack is set up in another segment. They're used internally by my debugger's build process.)
nasm -I ~/proj/lmacros/ test.asm -o test.bin -l testbin.lst -D_DOSENTRY=0 -D_RELOC=0 -D_BASESEGMENT=3000h -D_DOSEXIT=0 -D_RELOCEXE=0 -D_LARGEFILL && dosemu -K "$PWD" -E "ldebug /crcsip=3000_0000;lcs:ip /f test.bin" -dumb -td -kt
Build a flat format binary file, no DOS entry, use ROM-BIOS termination calls, and do not relocate at run time. The segment base is instead fixed to a particular address. The debugger command loads the file to that address. (Must be free!) For fun, do also fill the SECOND segment to 64 KiB.
As an addition, I tested loading the test.bin
file as a kernel from my bootable debugger. These are the commands I used:
nasm -l boot12.lst ~/proj/ldosboot/boot.asm -I ~/proj/lmacros/ -D_LOAD_NAME="'LDEBUG'" -o boot12.bin; nasm ~/proj/bootimg/bootimg.asm -I ~/proj/lmacros/ -o disk12.img -D_BOOTPATCHFILE=boot12.bin -D_PAYLOADFILE=../../../proj/ldebug/bin/ldebug.com,ldebug.sld,test.bin
qemu-system-i386 -fda disk12.img -boot order=a -display curses
This is the ldebug.sld
file:
:bootstartup
boot protocol freedos segment=3000 entry=0:0 test.bin
This boot
command modifies the FreeDOS load protocol (which says the whole file must be loaded) to load at the same segment we specified as base address, and sets the entry parameter as well for good measure. The filename points to our test binary.
The bootimg and ldosboot repos are found at https://hg.pushbx.org/ecm/ as well. The debugger is hosted at https://pushbx.org/ecm/web/#projects-ldebug
-f obj
to NASM and then a linker, or create the header and relocations manually with NASM's-f bin
. It is possible to set up everything yourself, including segment relocations, and still create a flat format (no header) .COM file. As to the actual file size, flat fornat .COM files larger than 64 KiB are likely not supported by the DOS you use.times 1024*1024 db 0x90
assembles just fine into a 1MiB flat binary with 2.16.01, presumably also with earlier NASM versions, so there isn't a hard 64K limit on output file size. Relative near jumps can't go more than +- 32KiB, so you might run into something that's not encodeable depending on your source code.