I am a JavaScript developer mainly, so I am familiar with object-oriented code and dealing with things you can see easily and interact with, like the GUI, even HTTP requests, etc. Plus you can put breakpoints around and inspect the object properties and such, or log it to the terminal, etc.
However, I would like to start getting good at Assembly. I have heard of GDB but have never needed to use it because I don't do any Assembly hardly.
I would like to practice by building things like (simplified) malloc, etc., plus simpler C stdlib functions like strcat
and whatnot. Ideally, too, I would like to avoid using any built-in stdlib functions to help me in the debugging process (like using printf
to print stuff out). If you ABSOLUTELY DO NOT recommend that I do that, that's one thing, but I would like to get a deeper sense of what it was like to be a programmer in the early days when the tools just didn't exist.
My question is, how did they debug this stuff when they were working on something like this Malloc I got from github:
has_initialized:
.zero 4
managed_memory_start:
.zero 8
last_valid_address:
.zero 8
malloc_init:
push rbp
mov rbp, rsp
mov edi, 0
call sbrk
mov QWORD PTR last_valid_address[rip], rax
mov rax, QWORD PTR last_valid_address[rip]
mov QWORD PTR managed_memory_start[rip], rax
mov DWORD PTR has_initialized[rip], 1
nop
pop rbp
ret
free:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-24], rdi
mov rax, QWORD PTR [rbp-24]
sub rax, 8
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 1
nop
pop rbp
ret
malloc:
push rbp
mov rbp, rsp
sub rsp, 48
mov QWORD PTR [rbp-40], rdi
mov eax, DWORD PTR has_initialized[rip]
test eax, eax
jne .L5
mov eax, 0
call malloc_init
.L5:
mov rax, QWORD PTR [rbp-40]
add rax, 8
mov QWORD PTR [rbp-40], rax
mov QWORD PTR [rbp-16], 0
mov rax, QWORD PTR managed_memory_start[rip]
mov QWORD PTR [rbp-8], rax
jmp .L6
.L9:
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rbp-24], rax
mov rax, QWORD PTR [rbp-24]
mov eax, DWORD PTR [rax]
test eax, eax
je .L7
mov rax, QWORD PTR [rbp-24]
mov eax, DWORD PTR [rax+4]
cdqe
cmp QWORD PTR [rbp-40], rax
jg .L7
mov rax, QWORD PTR [rbp-24]
mov DWORD PTR [rax], 0
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rbp-16], rax
jmp .L8
.L7:
mov rax, QWORD PTR [rbp-24]
mov eax, DWORD PTR [rax+4]
cdqe
add QWORD PTR [rbp-8], rax
.L6:
mov rax, QWORD PTR last_valid_address[rip]
cmp QWORD PTR [rbp-8], rax
jne .L9
.L8:
cmp QWORD PTR [rbp-16], 0
jne .L10
mov rax, QWORD PTR [rbp-40]
mov rdi, rax
call sbrk
mov rax, QWORD PTR last_valid_address[rip]
mov QWORD PTR [rbp-16], rax
mov rdx, QWORD PTR last_valid_address[rip]
mov rax, QWORD PTR [rbp-40]
add rax, rdx
mov QWORD PTR last_valid_address[rip], rax
mov rax, QWORD PTR [rbp-16]
mov QWORD PTR [rbp-24], rax
mov rax, QWORD PTR [rbp-24]
mov DWORD PTR [rax], 0
mov rax, QWORD PTR [rbp-40]
mov edx, eax
mov rax, QWORD PTR [rbp-24]
mov DWORD PTR [rax+4], edx
.L10:
add QWORD PTR [rbp-16], 8
mov rax, QWORD PTR [rbp-16]
leave
ret
I can imagine perhaps printing (using the Linux write
syscall) individual bytes one at a time. But that's far from anything I could use to visualize better what is going on. I am used to being way higher level and seeing the objects in an intuitive level (functions, objects, variables, etc.). But here everything is just bits and bit sequences (8, 16, 32, 64, etc.).
What are the best ways of debugging this to aid in your workflow? How did others in the past do this when the tooling wasn't available? How can you take advantage of low-level tooling (perhaps, like GDB) to do this? (Should you use GDB)? Or can you whip together a reasonable debugger on your own using only Assembly to help you somehow? Nothing fancy, but enough to help visualize something.
The second part of the question is, what do I try to focus on? That is, what do I want the debugger to do that will help me be more proficient? I am not sure what a good debugger would even do at this level. Maybe it just prints out the whole memory layout? And I then dig through the hexcode with my eyes? Is that what they did? Or do you get more strategic somehow?
Or do you just simply need to be able to run the code in your head, and that's it? I can't think of much in between.
My use case for this is to learn how to do Assembly like the ancient people did. But another use case would be to build out an OS on a bare-metal raspberry, I'm not sure if debuggers would work there (but perhaps they do). Either way, I would like this.