If you're building an algorithm from the C stdlib in pure Assembly (with no help), what is the best workflow for debugging/iterating?

softwareengineering.stackexchange https://softwareengineering.stackexchange.com/questions/410903

Question

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.

Was it helpful?

Solution

how did they debug this stuff

Same as today. You divide the code into trusted and untrusted and pick apart the untrusted until you trust it.

How did others in the past do this when the tooling wasn't available?

Tooling has always been available. It's just different tooling. Here's a blast from the past.

enter image description here

Note the highlighted "single step" control. Before we had software to help us debug we had hardware. Those lights on the panel aren't there to look cool in hollywood movies. They told us the state of the computer even when the output didn't. Testing was important enough that you could force the computer to stop on every instruction so you could write down its state.

You compared that to what you expected to see when you fed the computer known input. When state or output become unpredicted you stop trusting code.

Our tools and techniques have changed a lot over the decades but this pattern still holds true today. Just like us codemonkeys of old, I recommend you use every tool you can get your hands on.

OTHER TIPS

Never debug.

First: understanding your code is better than playing with a machine.

Second: knowing that you can’t debugger your code will make you write perspicuous code in the first place.

Evidence / experience:

In the batch days - send the cards, get the printout. If it differs: think.

In CP/M days - starting with a Basic-only machine, write an assembler in Basic, poke the code into memory, call it. If it never comes back, think harder next time. The end result was writing a whole BIOS, and indeed when it was put together with the rest, this Basic-only machine (Pertec PCC2000) was running CP/M.

On the 6502 - the Rockwell AIM65 had a thermal printer with lovely timing loops to get the dots in the right place. Rewrote it to be interrupt-driven (and twice as fast). Undebuggable by definition. Admittedly burned out one driver transistor doing it - but only one. Smoking transistors are a powerful incentive to get it right.

None of this is heroics. It is how things were done normally. Debuggers make bugs.

Don’t debug - think.

There were other debuggers before gdb e.g. IBM symbug, but I think they were all fairly similar. The only simpler approach is printf statements. Before that it was stacks of punched cards or paper tape. gdb works on other gnu compiler languages if you want to try it for those.

Licensed under: CC-BY-SA with attribution
scroll top