4
\$\begingroup\$

This fasm x64 (Linux) code seems very crude and repetitive, but it gets the job done. How could I perform this task in a more idiomatic manner?

format ELF64 executable 3

segment readable executable

entry $

prompt_user:
mov edx,prompt_len  
lea rsi,[prompt] ;<------ String
mov edi,1       ; STDOUT
mov eax,1       ; sys_write
syscall

get_user_input:
mov rax, 0 ; sys_read
mov rdi, 0 ; STDIN
lea rsi, [bit_string] ; <----- defined memory location
mov rdx, 2 ; <---- # of chars to read
syscall

;DEBUG CODE:
;lea rax, [bit_string]
;mov [rax], byte 'A'
;mov [rax+1], byte 'B'
lea rax, [bit_string]
cmp [rax], byte '0'
je load_0
cmp [rax], byte '1'
je load_1
cmp [rax], byte '2'
je load_2
cmp [rax], byte '3'
je load_3
cmp [rax], byte '4'
je load_4
cmp [rax], byte '5'
je load_5
cmp [rax], byte '6'
je load_6
cmp [rax], byte '7'
je load_7
cmp [rax], byte '8'
je load_8
cmp [rax], byte '9'
je load_9
cmp [rax], byte 'A'
je load_A
cmp [rax], byte 'B'
je load_B
cmp [rax], byte 'C'
je load_C
cmp [rax], byte 'D'
je load_D
cmp [rax], byte 'E'
je load_E
cmp [rax], byte 'F'
je load_F
load_0:
lea rsi, [n0]
jmp print_nibble
load_1:
lea rsi, [n1]
jmp print_nibble
load_2:
lea rsi, [n2]
jmp print_nibble
load_3:
lea rsi, [n3]
jmp print_nibble
load_4:
lea rsi, [n4]
jmp print_nibble
load_5:
lea rsi, [n5]
jmp print_nibble
load_6:
lea rsi, [n6]
jmp print_nibble
load_7:
lea rsi, [n7]
jmp print_nibble
load_8:
lea rsi, [n8]
jmp print_nibble
load_9:
lea rsi, [n9]
jmp print_nibble
load_A:
lea rsi, [nA]
jmp print_nibble
load_B:
lea rsi, [nB]
jmp print_nibble
load_C:
lea rsi, [nC]
jmp print_nibble
load_D:
lea rsi, [nD]
jmp print_nibble
load_E:
lea rsi, [nE]
jmp print_nibble
load_F:
lea rsi, [nF]
jmp print_nibble

nibble_two:
lea rax, [bit_string]
cmp [rax+1], byte '0'
je load_0
cmp [rax+1], byte '1'
je load_1
cmp [rax+1], byte '2'
je load_2
cmp [rax+1], byte '3'
je load_3
cmp [rax+1], byte '4'
je load_4
cmp [rax+1], byte '5'
je load_5
cmp [rax+1], byte '6'
je load_6
cmp [rax+1], byte '7'
je load_7
cmp [rax+1], byte '8'
je load_8
cmp [rax+1], byte '9'
je load_9
cmp [rax+1], byte 'A'
je load_A
cmp [rax+1], byte 'B'
je load_B
cmp [rax+1], byte 'C'
je load_C
cmp [rax+1], byte 'D'
je load_D
cmp [rax+1], byte 'E'
je load_E
cmp [rax+1], byte 'F'
je load_F

print_intro:
mov edx,intro_len   
lea rsi,[intro] 
mov edi,1       ; STDOUT
mov eax,1       ; sys_write
syscall
ret

print_nibble:
cmp [position], 0 ; If were on first iter, then print intro
jne skip_intro
push rsi
call print_intro
pop rsi

skip_intro:
mov rax, 1
cmp al, [position]
jl go_out
mov edx, 4  

mov edi,1       ; STDOUT
mov eax,1       ; sys_write
syscall
add [position], 1
jmp nibble_two

go_out:
mov edx,1   

lea rsi,[nl] 
mov edi,1       ; STDOUT
mov eax,1       ; sys_write
syscall

xor edi,edi     ; exit code 0
mov eax,60      ; sys_exit
syscall

segment readable writeable
prompt db 'Enter a byte in the format: F6', 0xA
prompt_len = $ - prompt
nl db 0xA
intro db 'In binary: ', 0
intro_len = $ - intro
bit_string rb 2
position db 0
n0 db '0000', 0
n1 db '0001', 0
n2 db '0010', 0
n3 db '0011', 0
n4 db '0100', 0
n5 db '0101', 0
n6 db '0110', 0
n7 db '0111', 0
n8 db '1000', 0
n9 db '1001', 0
nA db '1010', 0
nB db '1011', 0
nC db '1100', 0
nD db '1101', 0
nE db '1110', 0
nF db '1111', 0
\$\endgroup\$
6
\$\begingroup\$

In a high-level language, I would write it as:

def put_nibble(buf: array[byte], n: int):
    buf[0] = '0' + ((n shr 3) bitand 1)
    buf[1] = '0' + ((n shr 2) bitand 1)
    buf[2] = '0' + ((n shr 1) bitand 1)
    buf[3] = '0' + ((n shr 0) bitand 1)

def char_to_nibble(ch: byte) -> int:
    if '0' <= ch <= '9': return ch
    ch = ch bitor 0x20  # convert to lowercase
    if 'a' <= ch <= 'z': return ch - 'a' + 10
    error

def byte_to_binary(b: str):
    buf = byte[8]  # reserve memory on the stack
    hi = char_to_nibble(b[0])
    put_nibble(buf, hi)
    lo = char_to_nibble(b[1])
    put_nibble(but + 4, lo)
    sys_write(1, buf, 8)

This way you avoid comparing each possible digit on its own.

The above code also reduces memory access to the memory on the stack, since it doesn't need any external strings for the bit patterns.

Sure, the put_nibble code looks a bit repetitive, but you can merge all the '0' + … together into a single addition:

add dword [buf], 0x30303030  ; 0x30 == '0'

You could also compute the buffer for a whole nibble in a register and then write it to memory in a single instruction:

; input: al = the nibble to print
; output: ebx = 4 bytes ASCII buffer containing the 4 binary digits
xor ebx, ebx

rcr al, 1
adc ebx, '0'
rol ebx, 8

rcr al, 1
adc ebx, '0'
rol ebx, 8

rcr al, 1
adc ebx, '0'
rol ebx, 8

rcr al, 1
adc ebx, '0'
rol ebx, 8

mov [buf], ebx

Once you have translated the above high-level code into assembler, you probably want to optimize the code a bit. Or just use a compiler to do the heavy work for you. Write your code in C or in Go, assemble it into machine code and look at the result. Some keywords for searching:

  • go tool objdump
  • gcc -S binary.c -O2

By the way, you should not place the string literals for the prompt and the bit patterns in a writeable segment since they are not intended to be overwritten by any part of your program.

|improve this answer|||||
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.