Search code examples
debuggingassemblyx86nasmld

x86 procedure (32-bit) only works on certain parts of my code


All genuine solutions/advice/help is welcome. Thank you in advance.

Here is my basic question: Why does the 'ClrScreen' procedure at the bottom of my 'Print.asm' file not work in certain locations of my program, but does work in other locations?

Intended Behavior: I would like the 'ClrScreen' procedure in 'Print.asm' to work wherever it is called. The reason this procedure is important is because if there's ever an error encountered, I'd like the 'ClrScreen' procedure to execute to clear the Linux Terminal and output an error message.

1.) Clue 1 Write the instruction, 'call ClrScreen' as the first instruction in 'Battle_Chess.asm'. You will see that 'ClrScreen' works as intended right at this location in the program.

2.) Clue 2 Remove the 'call ClrScreen' instruction that we just previously wrote into the beginning of 'Battle_Chess.asm'. Now, write the same instruction 'call ClrScreen' after the instruction 'extern Rows'. 'ClrScreen' works as intended at this location in the program as well.

3.) Clue 3 This is where the conundrum occurs. Remove the 'call ClrScreen' instruction that we just previously wrote after the 'extern Rows' instruction. Write the instruction 'call ClrScreen' after the last 'mov DWORD [memory address], [immediate value]' instruction but before the 'call GetWinSize' instruction occurs in the 'Battle_Chess.asm' file. Finally, to see the bug remove the semi-colon from the 'mov DWORD [blackQueen + 4], 0x08000000' instruction. Then suddenly 'call ClrScreen' does not work anymore! You can go ahead and clear out all the semi-colons but not the ones for instructions 'mov DWORD [blackQueen + 4], 0x08000000' and 'mov DWORD [blackKing + 4], 0x10000000'. I do not understand how these instructions in particular can affect 'call ClrScreen'.

4.) Clue 4 Remove the 'call ClrScreen' instruction that we just previously wrote before the 'call GetWinSize' instruction. Put the semi-colon back on the two previous instructions that caused the bug mentioned in 'Clue 3'. Write the 'call ClrScreen' instruction after 'call GetWinSize' instruction. Again, 'call ClrScreen' is somehow affected by 'call GetWinSize'.

File: Battle_Chess.asm

; Program Author: David Mark Serrano
; Program Name: Battle_Chess
; Date Created: July 12, 2023
; Version: 1.0
; Purpose: To render a textual chessboard on the Linux Terminal for a game of chess against an AI opponent

; Built using file "makefile":
;
; Battle_Chess: Board_Render.o Battle_Chess.o Print.o
;   ld -m elf_i386 -o Battle_Chess Battle_Chess.o Board_Render.o Print.o
;
; Print.o: Print.asm
;   nasm -f elf32 -g -o Print.o Print.asm
;
; Board_Render.o: Board_Render.asm
;   nasm -f elf32 -g -o Board_Render.o Board_Render.asm
;
; Battle_Chess.o: Battle_Chess.asm
;   nasm -f elf32 -g -o Battle_Chess.o Battle_Chess.asm

section .data ; Section for initialized data

; File Descriptors
STDIN equ 0
STDOUT equ 1
STDERR equ 2

; Strings used in Battle Chess Engine
Terminal_Length db "Columns: ", 0
Minimum_Columns db "150", 0
Terminal_Height db "Rows: ", 0
Minimum_Rows db "45", 0
Error_Message_0x00000001 db "Error 0x00000001: Required terminal dimensions aren't met. Ensure $COLUMNS = 150 and $LINES = 45.", 0xA, 0 

; Chess Pieces
whitePawns db 8
whiteRooks db 8
whiteKnights db 8
whiteBishops db 8
whiteQueen db 8
whiteKing db 8
blackPawns db 8
blackRooks db 8
blackKnights db 8
blackBishops db 8
blackQueen db 8
blackKing db 8


section .text
    global _start

_start:

    nop ; This instruction is used to aid gdb debugger

    ; External references to global procedures defined in 'Board_Render.asm'
    extern GetWinSize

    ; External references to global procedural buffers defined in 'Board_Render.asm'
    extern WinSizeBuff

    ; External references to global procedures defined in 'Print.asm'
    extern PrintString
    extern PrintRows
    extern PrintColumns
    extern ClrScreen

    ; External references to global procedural buffers defined in 'Print.asm'
    extern Columns
    extern Rows

    ; This group of instructions (Line 53 - Line 76) set the initial positions of all game pieces on the Chessboard
    ; Each 8-byte variable is divided into high 32-bits and low 32-bits
    ; Each location in a variable where a bit is set directly corresponds to the game piece actively on that square
;   mov DWORD [whitePawns], 0x0000FF00 ; The White Pawns are actively on { Files A - H; Rank 2  }
;   mov DWORD [whitePawns + 4], 0x00000000 ; No White Pawns actively on these squares
;   mov DWORD [whiteRooks], 0x00000081 ; The white Rooks are actively on { File A; Rank 1 } and { File H - Rank 1 }
;   mov DWORD [whiteRooks + 4], 0x00000000 ; No White Rooks actively on these squares
;   mov DWORD [whiteKnights], 0x00000042 ; White Knights are actively on { File B; Rank 1 } and { File G; Rank 1 }
;   mov DWORD [whiteKnights + 4], 0x00000000 ; No White Knights actively on these squares
;   mov DWORD [whiteBishops], 0x00000024 ; White Bishops are actively on { File C; Rank 1 } { Rank F; File 1 }
;   mov DWORD [whiteBishops + 4], 0x00000000 ; No White Bishops actively on these squares
;   mov DWORD [whiteQueen], 0x00000008 ; White Queen is actively on { File D; Rank 1 }
;   mov DWORD [whiteQueen + 4], 0x00000000 ; No White Queen on these squares
;   mov DWORD [whiteKing], 0x00000010 ; White King is actively on { File E; Rank 1 }
;   mov DWORD [whiteKing + 4], 0x00000000 ; No White King on these squares
;   mov DWORD [blackPawns], 0x00000000 ; No Black Pawns on these squares
;   mov DWORD [blackPawns + 4], 0x00FF0000 ; Black Pawns are actively on { File A-H; Rank 7 }
;   mov DWORD [blackRooks], 0x00000000 ; No Black Rooks on these squares
;   mov DWORD [blackRooks + 4], 0x81000000 ; Black Rooks are actively on { File A; Rank 8 } { File H; Rank 8 }
;   mov DWORD [blackKnights], 0x00000000 ; No Black Knights on these squares
;   mov DWORD [blackKnights + 4], 0x42000000 ; Black Knights are actively on { File B; Rank 8 } { File G; Rank 8 }
;   mov DWORD [blackBishops], 0x00000000 ; No black Bishops on these squares
;   mov DWORD [blackBishops + 4], 0x24000000 ; Black Bishops are active on { File C; Rank 8 } { File F; Rank 8 }
;   mov DWORD [blackQueen], 0x00000000 ; No Black Queen on these squares
;   mov DWORD [blackQueen + 4], 0x08000000 ; Black Queen is actively on { File D; Rank 8 }
;   mov DWORD [blackKing], 0x00000000 ; No Black King on these squares
;   mov DWORD [blackKing + 4], 0x10000000 ; Black King is actively on { File E; Rank 8 }

    ; This instruction (Line XX) makes a call to procedure 'GWINSZ' in 'Board_Render.asm' file to retrieve dimensions of Linux Terminal
    call GetWinSize

    ; These instructions (Line XX - Line XX) will print the total $LINES of the current Linux Terminal session
    push Terminal_Length ; Push memory address of string 'Terminal_Length' onto stack
    push 0xA ; Push length of string "Terminal_Length' onto stack
    call PrintString ; Call procedure 'PrintString' from 'Print.asm' file
    call PrintColumns ; Call procedure 'PrintRows' from 'Print.asm' file

    ; These instructions (Line XX - Line XX) will ensure minimum Linux Terminal length is met, and if not met, the program will terminate
    xor eax, eax ; Clear 'eax' so that it is ready to store ASCII character in 'al'
    xor ecx, ecx ; Clear 'ecx' so that it is to be used as loop counter for 'CheckColumns' procedure
    xor edx, edx ; Clear 'edx' so that is is ready to store ASCII character in 'dl'
    CheckColumns:
    cmp ecx, 0x3 ; Check if 'ecx' has counted entire string "150"
    je CheckColumns.End
    mov al, BYTE [Columns + ecx] ; Copy contents of 'Columns' buffer in 'Print.asm' file to 'esi'
    mov dl, BYTE [Minimum_Columns + ecx] ; Copy contents of 'Minimum_Columns' in 'Battle_Chess.asm' buffer to 'edi'
    inc ecx ; Increase 'ecx' to update count for indexing into buffers 'Columns' and 'Minimum_Columns'
    cmp al, dl ; Compare ASCII character stored in 'al' with ASCII character stored in 'dl'
    jne Error_0x00000001 ; Jump to label 'Error_0x00000001' if 'al' and 'dl' are not equal
    je CheckColumns ; Repeat loop and jump to label 'Check_Columns' if 'al' and 'dl' are equal
    CheckColumns.End:

    ; These instructions (Line XX - Line XX) will print the total $COLUMNS of the current Linux Terminal session
    push Terminal_Height ; Push memory address of 'Terminal_Height' onto stack
    push 0x7 ; Push length of string 'Terminal_Height' onto stack 
    call PrintString ; Call procedure 'PrintString' from 'Print.asm' file
    call PrintRows ; Call procedure 'PrintColumns' from 'Print.asm' file

    ; These instructions (Line XX - Line XX) will ensure minimum Linux Terminal height is met, and if not met, the program will terminate
    xor eax, eax ; Clear 'eax' so that it is ready to store ASCII character in 'al'
    xor ecx, ecx ; Clear 'ecx' so that it is to be used as loop counter for 'CheckColumns' procedure
    xor edx, edx ; Clear 'edx' so that is is ready to store ASCII character in 'dl'
    CheckRows:
    cmp ecx, 0x2 ; Check if 'ecx' has counted entire string "45"
    je CheckRows.End
    mov al, BYTE [Rows + ecx] ; Copy contents of 'Columns' buffer in 'Print.asm' file to 'esi'
    mov dl, BYTE [Minimum_Rows + ecx] ; Copy contents of 'Minimum_Columns' in 'Battle_Chess.asm' buffer to 'edi'
    inc ecx ; Increase 'ecx' to update count for indexing into buffers 'Columns' and 'Minimum_Columns'
    cmp al, dl ; Compare ASCII character stored in 'al' with ASCII character stored in 'dl'
    jne Error_0x00000001 ; Jump to label 'Error_0x00000001' if 'al' and 'dl' are not equal
    je CheckRows ; Repeat loop and jump to label 'Check_Columns' if 'al' and 'dl' are equal
    CheckRows.End:

    nop ; This instruction is used to aid the GDB debugger

    ; This group of instructions (Line 97 - Line 97) will terminate the program and return code '0' to signal proper execution
    mov eax, 0x1 ; Specify sys_exit syscall
    xor ebx, ebx ; Clear `ebx` register so that it returns exit code 0
    int 0x80 ; Specify Vector Interrupt 128 (Kernel Services / Syscall Dispatcher)

    ; This group of instructions (Line XX - Line XX) will terminate the program and return code '1' to signal error
    Error_0x00000001:
    call ClrScreen ; Clear Linux Terminal so that Error_Message_0x00000001 is printed to 'STDOUT'
    push Error_Message_0x00000001 ; Push memory address of 'Error_Message_0x00000001' onto stack
    push 0x63 ; Push length of 'Error_Message' onto stack (99 characters)
    call PrintString ; Print string 'Error_Message' to STDOUT
    mov eax, 0x1 ; Specify sys_exit syscall 
    mov ebx, 0x1 ; Set 'ebx' register to so that it returns exit code 1
    int 0x80 ; Specify Vector Interrupt 128 (Kernel Services / Syscall Dispatcher)

File: Board_Render.asm

; Author/Programmer: David Mark Serrano
; Library: Board_Render
; Date Created: 07/21/2023
; Version: 1.0
; Purpose: This is a library with global variables and procedures for rendering the chessboard in the Battle Chess Engine

section .data ; Section for initialized data

; File Descriptor labels for easier referencing
STDIN equ 0 ; fildes 0 is assigned identifier `STDIN`
STDOUT equ 1 ; fildes 1 is assigned identifier `STDOUT`
STDERR equ 2 ; fildes 2 is assigned identifier `STDERR`

; ASCII character definitions for easier referencing
EOL equ 10 ; Linux end-of-line character
SPACECHR equ 32 ; ASCII space character
NULL equ 0 ; ASCII character for null-terminated strings

; Global declarations for 'GWINSZ' procedure and 'winsize_struct' buffer
global GetWinSize
global WinSizeBuff

; 'ioctl' codes
TIOCGWINSZ equ 0x5413

; Buffers needed for procedures
WinSizeBuff db 32

section .text ; x86 assembly instructions go here

GetWinSize:
mov eax, 0x36 ; Specify `ioctl` syscall in 32-bit x86 assembly
mov ebx, STDOUT ; STDOUT file descriptor stored in ebx
mov ecx, TIOCGWINSZ ; 'ioctl' code for retrieving size of Linux Terminal
lea edx, [WinSizeBuff] ; Load address of buffer 'winsize_struct' into register 'edx'
int 0x80 ; Vector Interrupt 128 (Kernel Services Dispatcher/Syscall Dispatcher)
ret ; Return from procedure

File: Print.asm

; Author/Programmer: David Mark Serrano
; Library: Print
; Date Created: 08/26/2023
; Version: 1.0
; Purpose: To define all procedures related to printing data to text via STDOUT

section .data ; Initialized data goes here

; File descriptors for STDIN/STDOUT/STDERR
STDIN equ 0
STDOUT equ 1
STDERR equ 2

; Escape sequences used in Print.asm
Clr db `\033[2J`, 0  ; Escape sequence to clear the screen followed by null terminator
ClrLen equ $ - Clr ; Calculate the length of the message

; Global declarations for 'ClrScreen' procedure
global ClrScreen


section .text ; x86 assembly instructions go here

; This procedure (Line XX - Line XX) will clear the Linux Terminal screen
ClrScreen:
mov eax, 0x4 ; Syscall number for write
mov ebx, STDOUT ; Specify 'STDOUT' or (File Descriptor 1) as the destination for the output
mov ecx, Clr ; Pointer to 'msg'
mov edx, ClrLen  ; Length of the 'msg'
int 0x80 ; Specify Vector Interrupt 128 (Kernel Services Dispatcher / Syscall Dispatcher 
ret ; Return to main 'Battle_Chess.asm'

Solution

  • blackQueen db 8 reserves 1 byte, initialized with value 0x08.

    You're running instructions that store past the end of the space you actually reserved in .data, stepping on the Clr: string, so when you write it to stdout you aren't writing the bytes of an ANSI escape sequence.

    Print.o is the last object file, so its .data goes right after the .data from other object files, meaning that mov DWORD [blackQueen + 4], imm32 can step on it.

    You can see this if you use strace like I suggested to see if the bytes written to stdout were still the escape sequence, and/or set a watchpoint in GDB on those bytes to see if anything modified them.

    You'll also see that putting Clr somewhere else, like in section .rodata where it belongs, will make your program still clear the screen on any call. (It won't fix the bug with your program not having reserved enough space in .data for its bitboards though, which will be a showstopper once you do anything with them, since stores to the earlier ones will step on later ones.)


    You want dq 0 to reserve 64 bits (8 bytes).

    Or better, you can put your bitboards from whitePawns to blackKing in .bss as resb 8 or resq 1 for each. ("reserve" pseudo-instructions take a number of elements to reserve, unlike dd = "data dword" or "define dword" which takes a list of values).

    You could put the initial conditions in .rodata to efficiently copy from when restarting a game, with rep movsb as a simple memcpy, or movaps 16-byte load/store instead of 2x mov-immediate per qword.

    Or for a single game, use correct non-zero initializers in .data, like WhitePawns: dq 0x000000000000FF00, and remove the instructions that store to them on startup.


    Related: