Files
klibc/src/console.asm

607 lines
14 KiB
NASM

%include "src/constants.asm"
extern strlen
extern strcat
extern itoa
extern utoa
section .rodata
mNL db NL
__fmt_BuffLen equ 128
section .bss
__fmt_Buff resb __fmt_BuffLen
__fmt_Args resq 5 ;5=>rsi,rdx,rcx,r8,r9
readsCBuff resb 1
section .text
global __INTERNAL_fmt
global print
global puts
global printf
global eprintf
global reads
;----- print(*str[]) -----;
; Prints given string to the console to stdout
; Return value: Amount of printed characters
; Used registers:
; rax* syscall >> (ret) amount of printed characters
; rdi* (arg) Pointer to str[] >> syscall arg (fd)
; rsi* syscall arg (pointer to str[])
; rdx* syscall arg (length of str[])
print:
sub rsp, SIZE_QWORD
mov rsi, rdi
call strlen
mov rdx, rax
mov eax, NR_write
mov edi, FD_stdout
syscall
add rsp, SIZE_QWORD
ret
;----- puts(*str[]) -----;
; Prints given string to the console to stdout and prints a new line
; Return value: Amount of printed characters
; Used registers:
; rax* syscall >> (ret) amount of printed characters
; rdi* (arg) Pointer to str[] >> syscall arg (fd)
; rsi* syscall arg (pointer to str[])
; rdx* syscall arg (length of str[]+1)
puts:
sub rsp, SIZE_QWORD
call print
mov esi, mNL
mov r10, rax
mov eax, NR_write
mov edi, FD_stdout
mov edx, 1
syscall
mov rax, r10
inc rax
add rsp, SIZE_QWORD
ret
;----- printf(*format[], ...) -----;
; Formats and prints given string to console to stdout
; Return value: Amount of printed characters
; Supported specifiers:
; %% Literal percentage sign
; %c Single character
; %d Signed integer, printed as decimal
; %i Alias for %d
; %u Unsigned integer, printed as decimal
; %x Unsigned integer, printed as hexadecimal number (lowercase)
; %X Unsigned integer, printed as hexadecimal number (uppercase)
; %b Unsigned integer, printed as binary number
; %o Unsigned integer, printed as octal number
; %p Print pointer, but truly alias for %#x -- this printf/udec processes all arguments as qwords so both %x and %p (and other specifiers) will always print full true value.
; That is also why %l/%ll is not supported - it's not needed.
; %s String
; %#{x} Use prefix for printed number (non-decimal). Supported specifiers: x, X, b, o. Works in combination with width specifier (see below)
; %[#]n* Pad left, n chars (maximum 64). Supported specifiers: d, i, u, x, X, b, o
; *[#]0n* Pad left with zeroes, n chars (maximum 64). Supported specifiers: d, i, u, x, X, b, o
; <!> Unsupported specifiers are printed as-is
; <!> For all specifiers (except %%) an argument is expected. Mismatch between arguments given and specifiers provided will lead to issues
;
; Supported special characters:
; \n New line
; \t Tab
; Used registers:
; rax* (ret) amount of printed characters
; rbx Length of array being written to (if rax != FD)
; rdi* (arg) pointer to format[] to format and print >> pointer to buffer
; rsi* (optional arg) >> Used for inserting strings to buffer
; rdx* (optional arg) >> misc
; rcx* (optional arg) >> Stores arg for %x/%X
; r8* (optional arg) >> Used for moving characters
; r9* (optional arg) >> Keeps track of where to jump after flushing buffer
; r10* Keeps track of current index of __fmt_Buff
; r11* Keeps track of total characters (return value)
; r12 Padding length
; r13 Bitmask, 1 = insert specifier yes/no, 2 = use zeroes for padding yes/no
; r14 Keeps track of amount of processed format specifiers
; r15 Stores fd to write output to (passed to __INTERNAL_fmt via RAX)
printf:
mov rax, FD_stdout
jmp __INTERNAL_fmt
;----- eprintf(*format[], ...) -----;
; Same as printf, except prints to stderr
eprintf:
mov rax, FD_stderr
jmp __INTERNAL_fmt
;----- __INTERNAL_fmt(*format[], ...) -----;
; See printf description above, +
; 1) Return value can be amount of printed characters or -errno (eg if bad FD was passed, -EBADF is returned)
; 2) FD has to be passed via RAX (this is clearly an INTERNAL function so passed arguments are slightly different, that is, RAX required for FD)
; 3) When writing a function(/wrapper) that calls this function, make sure that:
; - That function has function prologue (ie push rbp + mov rbp, rsp)
; - __INTERNAL_fmt is called via CALL and not a simple JMP like in printf
; - Arguments from function/wrapper are all shifted
; This is because in such I/O functions, first argument should be file descriptor, and following SYS V ABI, that first argument is passed to RDI
; However since __INTERNAL_fmt expects FD via RAX, all arguments have to be shifted. Due to this, argument in R9 becomes a stack argument
; Because of that, __INTERNAL_fmt has to load from stack with a different offset; +24 bytes extra (extra +8 for function call, extra +8 for function prologue and extra +8 to account for possible R9 arg becoming a stack arg)
;
; To make it even more complicated, instead of simply writing to a buffer and simply printing to given FD, it is now also possible to write to memory (array) directly (used for format)
; To achieve that, pass ~RAX (thus ~pointer_to_array) and push length of array to the stack (from within function wrapper). See format()
__INTERNAL_fmt:
%macro load_arg 1
cmp r14, 4
ja %%fromStack
mov %1, [__fmt_Args + SIZE_QWORD * r14]
jmp %%continue
%%fromStack:
cmp r15, FD_stderr
jle %%stackNoShift
test rbx, rbx
jnz %%stackShift_format
mov %1, [rbp + (RBP_OFFSET_CALLER*2 + SIZE_QWORD) + ((r14-5) * SIZE_QWORD)] ;Offset when called from wrapper like fwrite() (shifted args, output is to FD)
jmp %%continue
%%stackShift_format:
mov %1, [rbp + (RBP_OFFSET_CALLER*2 + SIZE_QWORD*4) + ((r14-5) * SIZE_QWORD)] ;Offset when called from wrapper like format() (shifted args, output is memory; extra push (+stack offset) for length of array)
;Not entirely sure why I need to do SIZE_QWORD*4 though, should be *3 (because of one push and one stack offset: 16 bytes)
;At %%fromStack I was also not sure why (according to comment at description: '+8 to account for possible R9 arg becoming a stack arg'); this does not apply
;It 1*8 is needed it checks out: 1 + 2 + that extra 1 = 4. Still not sure where the extra 8 is coming from.
jmp %%continue
%%stackNoShift:
mov %1, [rbp + RBP_OFFSET_CALLER + ((r14-5) * SIZE_QWORD)] ;Offset when called from wrapper like printf (no shifted args)
%%continue:
%endmacro
%macro push_regs 0
sub rsp, SIZE_QWORD
push rax
push rdi
push r9
push r10
push r11
%endmacro
%macro pop_regs 0
pop r11
pop r10
pop r9
pop rdi
pop rax
add rsp, SIZE_QWORD
%endmacro
%macro process_arg 3 ;process_arg [base] [upper/lowercase (r8)] [itoa/utoa]
load_arg rsi
push_regs
mov rdi, rsi
test r13, 1
jz %%baseNormal
mov rsi, ~%1
jmp %%args
%%baseNormal:
mov rsi, %1
%%args:
mov rdx, r12
test r13, 2
jz %%padSpaces
mov rcx, 1
jmp %%movr8 ;nice label name isn't it?
%%padSpaces:
xor rcx, rcx
%%movr8:
mov r8, %2
call %3
mov rsi, rax
pop_regs
jmp .insertString
%endmacro
%macro check_formatted_len 2 ;check_format_len [return_label] [offset]
test rbx, rbx
jz %%checkBufferLen
cmp r11, rbx
jae .wrapup
jmp .flushReturn_%1
%%checkBufferLen:
mov r9b, %1
cmp r10, __fmt_BuffLen-%2
je .flushBuffer
.flushReturn_%1:
%endmacro
%macro write_format_data 3 ;write_format_data [size] [offset] [what]
test rbx, rbx
jz %%writeToBuffer
mov %1 [r15 + %2], %3
jmp %%cnt
%%writeToBuffer:
mov %1 [__fmt_Buff + %2], %3
%%cnt:
%endmacro
; entry:
test rax, rax
js .start_fmt ;jump if signed; FD => Mem
cmp rax, 0
jg .start_fmt
mov rax, -EBADF
ret
.start_fmt:
push rbp
mov rbp, rsp
sub rsp, SIZE_QWORD
push rbx
push r12
push r13
push r14
push r15
mov r15, rax
xor rbx, rbx
test r15, r15
jns .checkEmptyStr
not r15
mov rbx, [rbp + RBP_OFFSET_CALLER]
.checkEmptyStr:
cmp byte [rdi], EOS
je .emptyStr
; Store arguments to memory - easier to load data + more available registers (= less stack usage)
mov [rel __fmt_Args + SIZE_QWORD * 0], rsi
mov [rel __fmt_Args + SIZE_QWORD * 1], rdx
mov [rel __fmt_Args + SIZE_QWORD * 2], rcx
mov [rel __fmt_Args + SIZE_QWORD * 3], r8
mov [rel __fmt_Args + SIZE_QWORD * 4], r9
xor rdx, rdx
xor r10, r10
xor r11, r11
xor r14, r14
.process:
cmp byte [rdi], EOS
je .wrapup
check_formatted_len 0,1
cmp byte [rdi], '\'
je .asciiReplacement
cmp byte [rdi], '%'
je .argReplacement
mov r8b, [rdi]
write_format_data byte,r10,r8b
inc r10
inc r11
inc rdi
jmp .process
;-- Replace special characters --;
.asciiReplacement:
cmp byte [rdi + 1], EOS
je .wrapup
mov r8, NL
cmp byte [rdi + 1], 'n'
cmove r9, r8
je .replaceAscii
mov r8, TAB
cmp byte [rdi + 1], 't'
cmove r9, r8
je .replaceAscii
jmp .invalidReplacement
.replaceAscii:
check_formatted_len 1, 1
write_format_data byte,r10,r8b
add rdi, 2
inc r10
inc r11
jmp .process
;-- Replace specifiers --;
.argReplacement:
cmp byte [rdi + 1], EOS
je .wrapup
cmp byte [rdi + 1], '%'
je .rep_pct
xor r12, r12
; Include specifier (r13 bit0=1) or not (bit0=0)
xor r13, r13
cmp byte [rdi + 1], '#'
jne .argGetPadZeroes
inc rdi
bts r13, 0
; Padding: zeroes (r13i bit1=1) or spaces (bit1=0)
.argGetPadZeroes:
cmp byte [rdi + 1], '0'
jb .checkReplArg
ja .padSpaces
bts r13, 1
.padSpaces:
cmp byte [rdi + 1], '9'
jg .checkReplArg
xor rcx, rcx
inc rdi
.findPaddingNumLen:
cmp byte [rdi], EOS
je .wrapup
cmp byte [rdi], '0'
jb .getPadding
cmp byte [rdi], '9'
ja .getPadding
inc rcx
inc rdi
jmp .findPaddingNumLen
.getPadding:
dec rdi
push rdi
push rax
xor rax, rax
mov r9, 1
.getPaddingLoop:
xor rax, rax
mov al, byte [rdi]
sub al, '0'
imul r9
add r12, rax
dec rdi
mov rax, r9
mov r9, 10
imul r9
mov r9, rax
loop .getPaddingLoop
pop rax
pop rdi
test r12, r12
jnz .checkReplArg
dec rdi
.checkReplArg:
cmp byte [rdi + 1], 'c'
je .rep_c
cmp byte [rdi + 1], 'i'
je .rep_d
cmp byte [rdi + 1], 'd'
je .rep_d
cmp byte [rdi + 1], 'u'
je .rep_d
cmp byte [rdi + 1], 'x'
je .rep_x
cmp byte [rdi + 1], 'X'
je .rep_x
cmp byte [rdi + 1], 'b'
je .rep_b
cmp byte [rdi + 1], 'o'
je .rep_o
cmp byte [rdi + 1], 'p'
je .rep_p
cmp byte [rdi + 1], 's'
je .rep_s
;--- Invalid ---;
.invalidReplacement:
test r12, r12
jz .irNoPadding
dec rdi
.irNoPadding:
cmp byte [rdi + 1], '\'
je .invalidReplacement_specialChar; '%\n' would become "'%','\','n'" instead of "'%',EOS" when inserting full invalid specifier.
mov r9w, word [rdi]
write_format_data word,r10,r9w
add rdi, 2
add r10, 2
add r11, 2
inc r14
jmp .process
.invalidReplacement_specialChar:
mov r9b, byte [rdi]
write_format_data byte,r10,r9b
inc rdi
inc r10
inc r11
inc r14
jmp .process
;--- '%%' ---;
.rep_pct:
mov sil, '%'
dec r14
jmp .insertChar
;--- '%c' ---;
.rep_c:
load_arg rsi
jmp .insertChar
;--- '%i' / '%d' / '%u' ---;
.rep_d:
cmp byte [rdi + 1], 'u'
je .unsigned
process_arg 10, 0, itoa
.unsigned:
process_arg 10, 0, utoa
;--- '%x' / '%X' ---;
.rep_x:
cmp byte [rdi + 1], 'x'
je .lower
process_arg 16, 1, utoa
.lower:
process_arg 16, 0, utoa
;--- '%b' ---;
.rep_b:
process_arg 2, 0, utoa
;--- '%o' ---;
.rep_o:
process_arg 8, 0, utoa
;--- '%p' ---;
.rep_p:
bts r13, 0 ;always force prefix, no matter if %p or %#p was used. Do not override padding though
process_arg 16, 0, utoa
;--- '%s' ---;
.rep_s:
load_arg rsi
;--- Insert string to buffer ---;
.insertString:
test rsi, rsi
jnz .doInsertString
check_formatted_len 2, 7
write_format_data byte,r10,'('
write_format_data qword,r10+1,'null'
write_format_data byte,r10+5,')'
add r10, 6
add r11, 6
jmp .endInsertString
.doInsertString:
cmp byte [rsi], EOS
je .endInsertString
check_formatted_len 3, 1
mov r8b, byte [rsi]
write_format_data byte,r10,r8b
inc rsi
inc r10
inc r11
jmp .insertString
.endInsertString:
inc r14
add rdi, 2
xor r12, r12
jmp .process
;--- Insert char to buffer ---;
.insertChar:
write_format_data byte,r10,sil
add rdi, 2
add r10, 1
add r11, 1
xor r12, r12
inc r14
jmp .process
.flushBuffer:
test rbx, rbx
jz .flush_print
sub rsp, SIZE_QWORD
push rdi
push rsi
push rdx
lea rdi, [r15]
lea rsi, [rel __fmt_Buff]
mov rdx, rbx
call strcat
pop rdx
pop rsi
pop rdi
add rsp, SIZE_QWORD
jmp .flush_ret
.flush_print:
push rdi
push rsi
push rdx
push r11
mov rax, NR_write
mov rdi, r15
lea rsi, [rel __fmt_Buff]
mov rdx, r10
syscall
pop r11
pop rdx
pop rsi
pop rdi
test rax, rax
js .quit
xor r10, r10
.flush_ret:
cmp r9b, 0
je .flushReturn_0
cmp r9b, 1
je .flushReturn_1
cmp r9b, 2
je .flushReturn_2
cmp r9b, 3
je .flushReturn_3
.wrapup:
test rbx, rbx
jz .wrapup_print
mov byte [r15+r10+1], EOS
lea rdi, [r15]
lea rsi, [rel __fmt_Buff]
mov rdx, rbx
sub rsp, SIZE_QWORD
push r11
call strcat
pop rax
add rsp, SIZE_QWORD
jmp .quit
.wrapup_print:
mov rax, NR_write
mov rdi, r15
lea rsi, [rel __fmt_Buff]
mov rdx, r10
mov r10, r11
syscall
test rax, rax
js .quit
mov rax, r10
jmp .quit
.emptyStr:
xor rax, rax
.quit:
pop r15
pop r14
pop r13
pop r12
pop rbx
add rsp, SIZE_QWORD
leave
ret
;----- reads(*inputBuffer[], len) -----;
; Reads from console (stdin), stores input to inputBuffer[]
; Return value: Amount of read characters
; Used registers:
; rax* (ret)
; rdi* (arg) Pointer to inputBuffer[]
; rsi* (arg) Size of inputBuffer[] (or less to read less characters)
; rdx* arg for syscall
; r12 Stores inputBuffer[]
; r13 Stores len
; r14 Counts amount of read characters
reads:
push r12
push r13
push r14
lea r12, [rdi]
mov r13, rsi
dec r13 ;account for EOS
xor r14, r14
.readLoop:
mov rax, NR_read
mov rdi, FD_stdin
lea rsi, [rel readsCBuff]
mov rdx, 1
syscall
mov al, [readsCBuff]
cmp al, NL
je .finish
cmp r14, r13
jae .readLoop ;Keep reading from stdin until \n is found as to flush stdin (too bad sys_lseek is not allowed on stdin, returns ESPIPE;illegal seek)
mov byte [r12], al
inc r12
inc r14
jmp .readLoop
.finish:
mov byte [r12], EOS
mov rax, r14
pop r14
pop r13
pop r12
ret