%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