Comparing 16 Bit Numbers in Nasm Produces Wrong Results

Comparing 16 bit numbers in nasm produces wrong results

Nasm does not keep record of the size of the data at label like some other assemblers. Suppose you enter 1, 2 and 3. Bytes stored at your labels will then be:

num1: db 0x31, 0x0A
num2: db 0x32, 0x0A
num3: db 0x33, 0x0A

When you move 32 bits of data from label num1, you are actually also moving data from num2. Because little endian machines store least significant bytes first, you get something like:

    mov ecx, 0x0A320A31 ; high bytes contain num2 and low bytes contain num1
cmp ecx, 0x0A330A32 ; high bytes contain num3 and low bytes contain num2
jg check_third
mov ecx, 0x0A330A32
check_third:
cmp ecx, 0x00000A33 ; high bytes contain res and low bytes contain num3
jg result
.....

resw 2 (or resd 1) would work because reserved memory is initialized to zeros. As Frank stated in the comment, you should use cl instead of ecx because 8 bits is all you need to handle in this case.

Comparing variables in NASM Assembly

The problem is that you are filling 32-bit registers with 32-bits of data when attempting to move the single byte from the strings into registers:

mov    eax,    [ str_number_a ]
mov ebx, [ str_number_b ]

If we have a look at the memory, there could be anything beyond the first byte of the strings:

xx xx xx 35 1F 4A 59 xx xx xx
^ ^^^^^^^
'5' Garbage

As eax and ebx are 32-bit registers, they read in both the character '5' and the garbage following. What does this mean? The since the garbage memory beyond the character '5' in both strings are very likely to be different, the comparison between eax and ebx will always be non-equal.

This can be fixed by changing the 32-bit/4 byte comparison (cmp) to an 8-bit/1 byte (or, single character) comparison:

cmp     byte eax, ebx

This way, only the first byte of each register will be compared.

EDIT:

Alternatively, you could use 8-bit registers to store data:

mov    al, [ str_number_a ]
mov ah, [ str_number_b ]

cmp al, ah

Comparing a 32 bit file size to 16 bit user input?

You're thinking about it backwards.

You want to zero-extend the 16-bit number to 32, and do a 32-bit comparison. (In C, operations on different types promote to a type that's wide enough to hold them both, because that's almost always what you want.)

As Jester explains, this means the upper 16 of your user input is always zero, so doing the upper half of the extended-precision / BigInteger comparison is simply checking that the upper half of the file size for non-zero. Then, if the file size is small enough that it might be below a 16-bit user input, compare the low half with the threshold.


You can also check for less-than with only one branch instruction by doing an extended-precision subtract:

file_loop:               ; do {
load a new file size

;; file size in dx:ax, threshold in cx
cmp ax, cx ; low half (non-destructive: cmp = sub but without writing ax)
sbb dx, 0 ; high half. (destructive)
; ZF reflects only the high-half compare result
; CF is useful (and so are SF/OF for signed compare).
; Avoid JCC conditions that depend on ZF, like JA.
jnb file_loop ; } while(CF==0); (JNB = JNC)

;;; dx:ax < 0:cx unsigned was true.
;;; So we know the original DX was 0 (and is now 0xFFFF)
;;; AX = the file size (which we didn't destroy), and we know it fits in 16 bits.

do something with this file
jmp file_loop

This is nice and compact, but probably not particularly better than cmp/jz and cmp/jb, especially if the 32-bit number is in memory. (You don't want to use sbb with a memory destination; that's less efficient.)


Converting 32->16 is trivial: just truncate by ignoring the upper 16 bits. But you seem to have already realized that wouldn't do what you want.

To answer your old title: to convert a 32-bit integer in dx:ax to a 16-bit integer in cx, use this instruction: mov cx, ax.

Assembly x86: comparing strings doesn't work

For comparing strings in x86-assembly there is a special OpCode named CMPS(Compare Strings). In your case of BYTE strings the relevant OpCode is CMPSB. You do use it by setting ESI to the source string and EDI to the destination string. The length of the equality check (preferrably the longest string) is set in ECX. Beware of overflows!.

So your code could look like this:

section .data
str1 db 'mystring',0
str1len equ $-str1
str2 db 'mystring',0

output db 'cmp went fine',0x0a,0
outputlen equ $-output
output2 db 'cmp went wrong',0x0a,0
output2len equ $-output2

section .text
global main

main:
lea esi, [str1]
lea edi, [str2]
mov ecx, str1len ; selects the length of the first string as maximum for comparison
rep cmpsb ; comparison of ECX number of bytes
mov eax, 4 ; does not modify flags
mov ebx, 1 ; does not modify flags
jne ifWrong ; checks ZERO flag

ifRight: ; the two strings do match
mov ecx, output
mov edx, outputlen
int 80h
jmp exit
ifWrong: ; the two strings don't match
mov ecx, output2
mov edx, output2len
int 80h
exit: ; sane shutdown
mov eax, 1
mov ebx, 0
int 80h

How to fix os.asm:113: error: TIMES value -138 is negative in assembly language

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:

  • The start up code properly sets up segment registers and uses the boot drive passed by the BIOS in the DL register. This is discussed in my Stackoverflow General Bootloader Tips
  • The stack is placed below the bootloader at 0x0000:0x7c00. Setting your own stack is important when reading data into memory outside 0x7c00 to 0x7dff since you don't know where the BIOS set the default stack (SS:SP).
  • Presents itself as a 1.44MB floppy with a BIOS Parameter Block to make it compatible with USB Floppy Drive Emulation booting on real hardware.
  • Stage2 is read a sector at a time using INT 13h/AH=2h starting at 0x07e00. It supports retry on errors.
  • Once Stage2 is finished loading the kernel, the bootloader transfers control to the stage2 code at 0x0000:0x7E00 (stage2_start)
  • Stage2 can contain the code you wish to run. You will have 32.5KiB of space to test your code rather than the limitations of a single boot sector (512 bytes).
  • Stage2's disk sectors immediately follow the boot sector in the disk image.
  • Your Stage2 (kernel) code goes into 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.
  • This bootloader will pass the original boot drive number (passed by the BIOS) in register DL to the code running in stage2.
  • The file 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.

The Code:

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.


Screenshots

The main menu appears as:

Sample Image

The credit screen appears as:

Sample Image


Notes:

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

comparing characters in assembly, nasm

JAE means jump if above or equal, that is to say when you compare to 'A', the jump will be taken for any character with an encoding greater or equal to 'A'.

What you want instead is JE, which means jump only if the values are exactly the same.

Assembly comparison flags understanding

Much easier to think of these in terms of 3 bit numbers, it all scales. Hmmm, if this is signed (you didnt specify/post in your high level code) then four bits is better because you used a 5. Walk through the numbers near 5 (this shows the output of the alu)

cmp reg,5
0111 - 0101 = 0111 + 1010 + 1 = 10010
0110 - 0101 = 0110 + 1010 + 1 = 10001
0101 - 0101 = 0101 + 1010 + 1 = 10000
0100 - 0101 = 0100 + 1010 + 1 = 01111
0011 - 0101 = 0011 + 1010 + 1 = 01110

Now you have to understand how the hardware works. Some processor families when you do a subtract invert the carry flag coming out of the alu, others dont. either way you can definitely see a state change at the 5 - 5 point. And you dont need the carry flag here anyway, code doesnt use it.

In case you are doing signed math, then try some negative numbers as well.

0000 - 0101 = 0000 + 1010 + 1 = 01011  
1111 - 0101 = 1111 + 1010 + 1 = 11010
1110 = 0101 = 1110 + 1010 + 1 = 11001

And that sheds some light on the problem.

signed overflow is defined as the carry in being not equal to the carry out on the msbit of the adder. That can get messy so we just need to know where that boundary is.

0111 - 0101 = 7 - 5 = 2
0110 - 0101 = 6 - 5 = 1
0101 - 0101 = 5 - 5 = 0
0100 - 0101 = 4 - 5 = -1
0011 - 0101 = 3 - 5 = -2

and so on. Using this 4 bit model, in a signed interpretation we are limited to +7 (0b0111) down to -8 (0b1000). So the after -3 - 5 we will get into trouble:

1110 - 0101 = 1110 + 1010 + 1 = 11001 , -2 - 5 = -7
1101 - 0101 = 1101 + 1010 + 1 = 11000 , -3 - 5 = -8
1100 - 0101 = 1100 + 1010 + 1 = 10111 , -4 - 5 = 7 (-9 if we had more bits)
1011 - 0101 = 1011 + 1010 + 1 = 10110 , -5 - 5 = 6 (-10 if we had more bits)
1010 - 0101 = 1010 + 1010 + 1 = 10101 , -6 - 5 = 5 (-11 if we had more bits)
1001 - 0101 = 1001 + 1010 + 1 = 10100 , -7 - 5 = 4 (-12 if we had more bits)
1000 - 0101 = 1000 + 1010 + 1 = 10011 , -8 - 5 = 3 (-13 if we had more bits)

The latter five are a signed overflow, the signed result cannot be represented in the number of bits available. (remember we are playing with a four bit system for now, that top bit is the carry bit, visually remove it when you look at the result).

The signed flag is simply the msbit of the result, which is also changing a the interesting boundaries. Cases where the signed flag, (msbit of result) is set is the positive (eax) values below 5 and the negative numbers that do not result in a signed overflow (+4 down to -3). All of which are in the <5 category so they want to have a result of 2. The first test looks for cases where sign is set, why it bothers to then test the signed overflow? That makes no sense, we already know all signed results are in the less than 5 category. the extra jump if signed overflow doesnt hurt.

so if you fall through js signon then the sign bit is off which is numbers greater than or equal to 5 (want a result of 1) or results negative enough to cause a signed overflow (want a result of 2). so jo elseblock sorts these two cases out by picking up the result of 2 cases (signed overflow, very negative). and jmp thenblock takes the positive numbers above 5.

It looks to me like you are doing signed math here (somewhat obvious from using the signed overflow flag). Since you are using a 5 to compare against and signed math, you need 4 or more bits in your system to implement this code, so 8, 32, 64, 123456 bits, it doesnt matter it all works the same as a 4 bit system (for this comparision). I find it easier to minimize the number of bits to do the analysis. Hardcoded comparisons like this make it that much easier, as above hand compute results just above, at, and below. then walk through the all zeros (zero) to all ones (minus one) for signed numbers, and very negative into the signed overflow range. for unsigned numbers it is a bit easier but the same process.

How can I compare the first character of a string with another character in x86-64 assembly?

I'll explain the changes side-by-side (hopefully that's easier to follow):

global start

section .data
msg: db "Hello, World!", 10, 0

section .text
start:
mov rdx, msg
mov al, [rdx] ; moves one byte from msg, H to al, the 8-bit lower part of ax
mov ah, 'H' ; move constant 'H' to the 8-bit upper part of ax
cmp al, ah ; compares H with H
je equal ; yes, they are equal, so go to address at equal

mov rax, 0x2000001
mov rdi, [rdx]
syscall

equal: ; here we are
mov rax, 0x2000001
mov rdi, 58
syscall

If you're not understanding the use / mention of al, ah, ax, please see General-Purpose Registers.



Related Topics



Leave a reply



Submit