I\'m developing an operating system in assembly language. At a certain time i get this error from NASM:
os.asm:113: error: TIMES value -138 is negati
TL;DR : Your code and data is too big and collided with the boot signature in the last 2 bytes of the file. The code below is a floppy disk bootloader that reads a second stage (your kernel) and transfers control to it. The provided BPB is for a 1.44MiB floppy. Unlike a bootloader, stage2 will be loaded to physical address 0x07e00 (right after the bootloader in memory). This allows your code to be up to 32.5KiB in size. Your second stage can read more sectors if needed. This code has been designed so others can use this as a template for reading a second stage and transferring control to it.
This question has actually been already answered under your previous Stackoverflow Question. There is a warning about the padding using times 512 - ($ - $$) db 0x00
needing to be 510 and not 512. The answer warns of too much code and data (exceeding 512 bytes), and a way to get better error/warnings from NASM about the size. The note in my other answer summarizes the size issue as:
If the file os.bin is more than 512 bytes then you will need to use the BIOS to read more disk sectors into memory manually. The disk reads from a floppy can be done with INT 13h/AH=2h.
What wasn't provided was a mechanism (example) that uses NASM and INT 13h/AH=2h to read more disk sectors (aka stage2) into memory right after the bootloader at physical address 0x07E00. The code is commented, but it effectively does:
stage2_start
)stage2.asm
. stage2.asm
gets assembled into stage2.bin
and os.asm
includes the binary file stage2.bin
so that the size of stage2 can be determined for purposes of loading it into memory by the bootloader.stage2.asm
must use ORG 0x7e00
since the process above will be loading this code to 0x7e00, so the ORG (origin point) must be set to match.stage2info.inc
defines constants to determine what the origin point of stage2 is, and what segment and offset should be used for the FAR JMP when transferring control to it. The default version of this file assumes stage2 is accessed via 0x0000:0x7e00. An alternative version2 of the file can be used to make that 0x07e0:0x0000. The latter version allows your code to take up a full 64kb segment.bpb.inc:
jmp boot_start
TIMES 3-($-$$) DB 0x90 ; Support 2 or 3 byte encoded JMPs before BPB.
bpb_disk_info:
; Dos 4.0 EBPB 1.44MB floppy
OEMname: db "mkfs.fat" ; mkfs.fat is what OEMname mkdosfs uses
bytesPerSector: dw 512
sectPerCluster: db 1
reservedSectors: dw 1
numFAT: db 2
numRootDirEntries: dw 224
numSectors: dw 2880
mediaType: db 0xf0
numFATsectors: dw 9
sectorsPerTrack: dw 18
numHeads: dw 2
numHiddenSectors: dd 0
numSectorsHuge: dd 0
driveNum: db 0
reserved: db 0
signature: db 0x29
volumeID: dd 0x2d7e5a1a
volumeLabel: db "NO NAME "
fileSysType: db "FAT12 "
stage2info.inc:
STAGE2_ABS_ADDR equ 0x07e00 ; Physical address of stage2
; Segment and Offset to use to transfer (FAR JMP) control to Stage2
; Segment:Offset = 0x0000:0x7e00
STAGE2_RUN_SEG equ 0x0000
STAGE2_RUN_OFS equ STAGE2_ABS_ADDR
os.asm:
%include "stage2info.inc"
STAGE2_LOAD_SEG equ STAGE2_ABS_ADDR>>4
; Segment to start reading Stage2 into
; right after bootloader
STAGE2_LBA_START equ 1 ; Logical Block Address(LBA) Stage2 starts on
; LBA 1 = sector after boot sector
STAGE2_LBA_END equ STAGE2_LBA_START + NUM_STAGE2_SECTORS
; Logical Block Address(LBA) Stage2 ends at
DISK_RETRIES equ 3 ; Number of times to retry on disk error
bits 16
ORG 0x7c00
; Include a BPB (1.44MB floppy with FAT12) to be more comaptible with USB floppy media
%include "bpb.inc"
boot_start:
xor ax, ax ; DS=SS=ES=0 for stage2 loading
mov ds, ax
mov ss, ax ; Stack at 0x0000:0x7c00
mov sp, 0x7c00
cld ; Set string instructions to use forward movement
; Read Stage2 1 sector at a time until stage2 is completely loaded
load_stage2:
mov [bootDevice], dl ; Save boot drive
mov di, STAGE2_LOAD_SEG ; DI = Current segment to read into
mov si, STAGE2_LBA_START ; SI = LBA that stage2 starts at
jmp .chk_for_last_lba ; Check to see if we are last sector in stage2
.read_sector_loop:
mov bp, DISK_RETRIES ; Set disk retry count
call lba_to_chs ; Convert current LBA to CHS
mov es, di ; Set ES to current segment number to read into
xor bx, bx ; Offset zero in segment
.retry:
mov ax, 0x0201 ; Call function 0x02 of int 13h (read sectors)
; AL = 1 = Sectors to read
int 0x13 ; BIOS Disk interrupt call
jc .disk_error ; If CF set then disk error
.success:
add di, 512>>4 ; Advance to next 512 byte segment (0x20*16=512)
inc si ; Next LBA
.chk_for_last_lba:
cmp si, STAGE2_LBA_END ; Have we reached the last stage2 sector?
jl .read_sector_loop ; If we haven't then read next sector
.stage2_loaded:
mov ax, STAGE2_RUN_SEG ; Set up the segments appropriate for Stage2 to run
mov ds, ax
mov es, ax
; FAR JMP to the Stage2 entry point at physical address 0x07e00
jmp STAGE2_RUN_SEG:STAGE2_RUN_OFS
.disk_error:
xor ah, ah ; Int13h/AH=0 is drive reset
int 0x13
dec bp ; Decrease retry count
jge .retry ; If retry count not exceeded then try again
error_end:
; Unrecoverable error; print drive error; enter infinite loop
mov si, diskErrorMsg ; Display disk error message
call print_string
cli
.error_loop:
hlt
jmp .error_loop
; Function: print_string
; Display a string to the console on display page 0
;
; Inputs: SI = Offset of address to print
; Clobbers: AX, BX, SI
print_string:
mov ah, 0x0e ; BIOS tty Print
xor bx, bx ; Set display page to 0 (BL)
jmp .getch
.repeat:
int 0x10 ; print character
.getch:
lodsb ; Get character from string
test al,al ; Have we reached end of string?
jnz .repeat ; if not process next character
.end:
ret
; Function: lba_to_chs
; Description: Translate Logical block address to CHS (Cylinder, Head, Sector).
; Works for all valid FAT12 compatible disk geometries.
;
; Resources: http://www.ctyme.com/intr/rb-0607.htm
; https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
; https://stackoverflow.com/q/45434899/3857942
; Sector = (LBA mod SPT) + 1
; Head = (LBA / SPT) mod HEADS
; Cylinder = (LBA / SPT) / HEADS
;
; Inputs: SI = LBA
; Outputs: DL = Boot Drive Number
; DH = Head
; CH = Cylinder (lower 8 bits of 10-bit cylinder)
; CL = Sector/Cylinder
; Upper 2 bits of 10-bit Cylinders in upper 2 bits of CL
; Sector in lower 6 bits of CL
;
; Notes: Output registers match expectation of Int 13h/AH=2 inputs
;
lba_to_chs:
push ax ; Preserve AX
mov ax, si ; Copy LBA to AX
xor dx, dx ; Upper 16-bit of 32-bit value set to 0 for DIV
div word [sectorsPerTrack] ; 32-bit by 16-bit DIV : LBA / SPT
mov cl, dl ; CL = S = LBA mod SPT
inc cl ; CL = S = (LBA mod SPT) + 1
xor dx, dx ; Upper 16-bit of 32-bit value set to 0 for DIV
div word [numHeads] ; 32-bit by 16-bit DIV : (LBA / SPT) / HEADS
mov dh, dl ; DH = H = (LBA / SPT) mod HEADS
mov dl, [bootDevice] ; boot device, not necessary to set but convenient
mov ch, al ; CH = C(lower 8 bits) = (LBA / SPT) / HEADS
shl ah, 6 ; Store upper 2 bits of 10-bit Cylinder into
or cl, ah ; upper 2 bits of Sector (CL)
pop ax ; Restore scratch registers
ret
; Uncomment these lines if not using a BPB (via bpb.inc)
; numHeads: dw 2 ; 1.44MB Floppy has 2 heads & 18 sector per track
; sectorsPerTrack: dw 18
bootDevice: db 0x00
diskErrorMsg: db "Unrecoverable disk error!", 0
; Pad boot sector to 510 bytes and add 2 byte boot signature for 512 total bytes
TIMES 510-($-$$) db 0
dw 0xaa55
; Beginning of stage2. This is at 0x7E00 and will allow your stage2 to be 32.5KiB
; before running into problems. DL will be set to the drive number originally
; passed to us by the BIOS.
NUM_STAGE2_SECTORS equ (stage2_end-stage2_start+511) / 512
; Number of 512 byte sectors stage2 uses.
stage2_start:
; Insert stage2 binary here. It is done this way since we
; can determine the size(and number of sectors) to load since
; Size = stage2_end-stage2_start
incbin "stage2.bin"
; End of stage2. Make sure this label is LAST in this file!
stage2_end:
You place all the code you want to test in the file stage2.asm
which will be included by my version of os.asm
. A version of your code with the unnecessary parts at the beginning and end removed is:
stage2.asm
%include "stage2info.inc"
ORG STAGE2_RUN_OFS
BITS 16
start:
; Removed the segment and stack code
call cls
MOV AH, 06h ; Scroll up function
XOR AL, AL ; Clear entire screen
XOR CX, CX ; Upper left corner CH=row, CL=column
MOV DX, 184FH ; lower right corner DH=row, DL=column
MOV BH, 1Eh ; YellowOnBlue
INT 10H
mov si, text_string ; Put string position into SI
call print_string ; Call our string-printing routine
push bx ;push registers
push cx
push dx
mov ah,0h
int 16h
cmp al, '1'
je reboot
cmp al, '2'
je shutdown
cmp al, '3'
je about
cmp al, '4'
je message
cmp al, '5'
je shutdown
cmp al, '6'
je credits
jmp $ ; Jump here - infinite loop!
text_string db '|Main Menu| |Smile OS V1.4|',13,10,'1) Reboot',13,10,'2) Shutdown',13,10,'3) About',13,10,'4) Message',13,10,'5) System Halt',13,10,'6) Credits',0
about_string db '|About|',13,10,'Smile OS is a console based operating system in assembly language. 8 hours of intense work done by Alex~s Software. Many errors but solved and very successful.',13,10,'Press any key to go back!',0
message_str db '|Message|',10,13,'Hello, World!',13,10,'Press any key to go back!',0
cr_str db '|Credits|',13,10,'Copyright © 2018 Alex~s Software',13,10,'Main Programer: Alex',13,10,'Graphichs: What graphics?',13,10,'Idea: nobody :)',0
reboot:
mov ax, 0
int 19h
shutdown:
mov ax, 0x1000
mov ax, ss
mov sp, 0xf000
mov ax, 0x5307
mov bx, 0x0001
mov cx, 0x0003
int 0x15
credits:
call cls
mov si, cr_str ; Put string position into SI
call print_string ; Call our string-printing routine
push bx ;push registers
push cx
push dx
mov ah,0h
int 16h
je start
message:
call cls
mov si, message_str ; Put string position into SI
call print_string ; Call our string-printing routine
push bx ;push registers
push cx
push dx
mov ah,0h
int 16h
je start
cls:
pusha
mov ah, 0x00
mov al, 0x03 ; text mode 80x25 16 colours
int 0x10
popa
ret
about:
call cls
mov si, about_string ; Put string position into SI
call print_string ; Call our string-printing routine
push bx ;push registers
push cx
push dx
mov ah,0h
int 16h
je start
print_string: ; Routine: output string in SI to screen
mov ah, 0Eh ; int 10h 'print char' function
.repeat:
lodsb ; Get character from string
cmp al, 0
je .done ; If char is zero, end of string
int 10h ; Otherwise, print it
jmp .repeat
.done:
ret
You then assemble and build the disk image with these commands1:
# Build stage2 (kernel) FIRST as os.asm will include stage2.bin
nasm -f bin stage2.asm -o stage2.bin
# Build and combine stage1 (boot sector) and stage2 (kernel)
nasm -f bin os.asm -o os.bin
# Build 1.44MB disk image
dd if=/dev/zero of=disk.img bs=1024 count=1440
dd if=os.bin of=disk.img conv=notrunc
Lines starting with #
are just comments and are not commands.
The main menu appears as:
The credit screen appears as:
1You use these commands which contains an error:
nasm os.asm -f bin -o os.bin
dd if=/dev/zero of=os.img bs=1024 count=1440
dd if=os.bin of=os.img
The last line should be dd if=os.bin of=os.img conv=notrunc
so that the 1.44MB disk image doesn't get truncated when the os.bin
file is written to it. If you look at the size of your disk image you will probably see that is not the expected 1474560.
2An alternative stage2info.inc
file to use 0x07e0:0x0000 instead of 0x0000:0x7e00 to transfer control to stage2:
STAGE2_ABS_ADDR equ 0x07e00 ; Physical address of stage2
; Segment and Offset to use to transfer (FAR JMP) control to Stage2
; Segment:Offset = 0x07e0:0x0000
STAGE2_RUN_SEG equ STAGE2_ABS_ADDR>>4
STAGE2_RUN_OFS equ 0x0000
Since your line:
times 512 - ($ - $$) db 0
is meant to fill up the rest of the 512-byte chunk of memory with zeroes, it's likely that you have already exceeded that (by roughly 138 bytes). You'll probably need to just shorten your code (or make some of those strings a little less verbose) so it fits.
My advice would be to start with about_string
, which seems way more than necessary. Removing the (rather self-serving)" 8 hours of intense work done by Alex~s Software. Many errors but solved and very successful."
would be a good start as it would save 93 bytes. In addition, at the cost of a few extra bytes of code, you could remove the duplicate "Press any key to go back!"
(with leading and trailing CRLF).
This could be done with something like:
about_string db '|About|',13,10,'Smile OS is a console based operating system in assembly language.'
any_key db 13,10,'Press any key to go back!',0
message_str db '|Message|',10,13,'Hello, World!',0
The about string could then be printed in exactly the same manner (because about_string
has no terminating 0
hence will also print any_key
) but the message string would change into a two-step operation:
mov si, message_str --> mov si, message_str
call print_string call print_string
mov si, any_key
call print_string
That will save about another 20 bytes, giving you a saving of about 113 of the 138 bytes.
Other than that, there appear to be a few minor things that could save very small amounts of space such as converting:
mov ah, 0x00
mov al, 0x03
into:
mov ax, 0x0003
or refactoring the key input into a function (this will also keep your stack balanced, something your current code does not appear to do, although I'm not actually sure that it's necessary - the documentation seems to suggest that ax
is the only register affected, meaning you could probably remove the pushes and pops):
get_kbd: push bx
push cx
push dx
xor ax,ax
int 16h
je start
pop dx
pop cx
pop bx
ret
Of course, if you do all that and you still can't get below the threshold, there's nothing requiring you to put the string in the boot code area. You could just as easily store them on another area which the boot code loads in as the first step. That way, you take away all the strings from the boot code area, saving some 460-odd bytes (adding maybe twenty back for the code to load the string sectors) and therefore coming in well under the threshold.
It's negative because 510 - code_size
is negative. Your code is too big to fit in one sector as an MBR.
I commented out the padding line, and assembled your file. The resulting binary is 652 bytes long (including the 2 bytes after the padding). 512 - 650 = -138
.
Either code-golf your program so it does the same thing with fewer code bytes (Tips for golfing in x86/x64 machine code), or break it up into a boot sector that loads the rest of the code from disk after booting with BIOS calls.
With all those long strings, there's probably not a lot of room for saving 140 bytes here. There's certainly room for significant saving, e.g. mov ax, 07C0h
/ add ax, 288
is silly vs. mov ax, 07C0h + 288
so there's 3 bytes you can save easily.
See Enable the boot loader to load the second sector of a USB and How to load kernel or be able to use more space in own bootloader?
Michael Petch's general bootloader development tips (Boot loader doesn't jump to kernel code) should be helpful if you want to mess around with legacy BIOS stuff.
Your other option is to write a UEFI bootloader instead of legacy BIOS, so your code starts in 32 or 64-bit mode. And more importantly, an EFI "application" can be any reasonable size, so the firmware can load all your code at once instead of you having to write code that loads the rest of itself.
Also, you incorrectly used 512 - size
, which won't leave room for the MBR signature 2 bytes at the end. Use 510 - ($ - $$)
See Assembly Why when i modify the code the result gets unbootable for more about that.