0

I wrote a simple recursive print function in x86 assembly, however, I noticed that all the instructions after call print never get executed, after scratching my head over it, I figured that if I remove push ax, pop ax everything works fine, is there something I miss about the stack?

Here is the code:

print:
    push ax
    mov ah, 0x0e
    mov al, [bx+si]
    inc si
    cmp al, 0x00
    int 0x10
    jne print
    mov al, 0x0d
    int 0x10
    mov si,0
    pop ax
    ret

it's a simple recursive function with the base condition of the null character, the parameter is bx, while the counter is SI.

  • 4
    That's not recursion because you do not `call` your function. It's a loop. And you have a `push ax` in the loop so it will push multiple things and then you only pop one thing. Thus, you unbalance the stack. You also have the `int 0x10` in the wrong place between the `cmp` and the `jne`. – Jester Jun 08 '21 at 19:06
  • 3
    Yes. This is correct behavior. For every `push` there has to be a `pop` (generally speaking). If your loop has more pushes than pops, the `ret` returns to another address, because both use the same stack. – zx485 Jun 08 '21 at 19:08
  • Thank you for your response. I am just a beginner in it, so I don't know how to do a conditional call after ```cmp```, can you please help me? And I assumed that the function is recursive that's why I put ```int 0x10``` before the jump statement, but how can I print it in an iterative way? I should pop every time before the jump statement? That seems way too inefficient, is there a better way? – MinorWannaBeDev Jun 08 '21 at 19:12
  • 1
    Why do you need this to be recursive? Also even if you need it to be recursive, why do you need to save `ax`? – Jester Jun 08 '21 at 19:18
  • I've just started assembly, so I need to learn these basics like recursion, stack, loops functions, etc... – MinorWannaBeDev Jun 08 '21 at 19:24
  • And about ax, it's just that I want to keep the code clean, to make the function have as little interference as possible, since it's part of a bootsector. – MinorWannaBeDev Jun 08 '21 at 19:25
  • 1
    recursion is not the first thing on the basics list, it is not on the basics list. so try basics first. – old_timer Jun 08 '21 at 19:26
  • I've used your tips and fixed the code (made it iterative), I will post the fixed code in no time! Thank your for saving me :D – MinorWannaBeDev Jun 08 '21 at 19:26
  • Once you have the basics, then recursion is automatic as to how it works it is no different than calling any other function. – old_timer Jun 08 '21 at 19:26
  • My issue is that I don't know how to use ```call function``` after a cmp instruction instead of using jmp, is there a way to achieve it? – MinorWannaBeDev Jun 08 '21 at 19:32
  • Brief answer as to the stack: After `push ax` add a label `.loop:` (leading dot means local label in NASM) and jump to it using `jne .loop` instead. – ecm Jun 08 '21 at 19:36
  • Indeed there is no conditional call instruction. So instead you use a conditional jump with the opposite sense to jump over the call if you don't want it. `cmp al, 0x00` / `je past_call` / `call print` / `past_call: ;rest of code` – Nate Eldredge Jun 08 '21 at 21:52
  • 1
    *keep the code clean, to make the function have as little interference as possible, since it's part of a bootsector.* - A boot sector is at most 510 bytes of code. That's small enough to just use comments on each function to specify which registers it does/doesn't modify. If you want all your functions to have a common calling convention, it's a good idea for that convention to let AX and DX (and maybe CX) be call-clobbered, so loops that want to call functions can keep their data in any other register if they want it to survive. – Peter Cordes Jun 08 '21 at 22:13
  • 1
    Filling all your functions with push/pop of everything isn't "clean". See also [call-preserved vs. call-clobbered registers in a calling convention](https://stackoverflow.com/questions/9268586/what-are-callee-and-caller-saved-registers/56178078#56178078). But anyway, yeah your answer at least keeps the push/pop out of the loop, which doesn't help for code-size but is much better for performance. – Peter Cordes Jun 08 '21 at 22:14

1 Answers1

1

Thanks to the help of the kind folk in the comment section I was able to figure out a way to do it iteratively.

print:
    push ax
    mov ah, 0x0e
    mov al, [bx+si]
    inc si
    cmp al, 0x00
    int 0x10
    pop ax
    jne print
    push ax
    mov ah, 0x0e
    mov al, 0x0d
    int 0x10
    xor si,si
    pop ax
    ret

Very inefficient and hacky, but hey it works!

Edit:

I used ECM's tip and here is a more efficient implementation:

print:
    push ax
    push si
    .loop:
    mov ah, 0x0e
    mov al, [bx+si]
    inc si
    cmp al, 0x00
    int 0x10
    jne .loop
    mov ah, 0x0e
    mov al, 0x0d
    int 0x10
    pop si
    pop ax
    ret

It's way cleaner and more efficient.

  • Your first version zeros SI for no apparent reason, instead of returning the end of the string as a bonus return value that a caller could use to find out how many bytes were printed. Is the caller supposed to have zeroed SI for you? You don't initialize it. You could just save/restore BX, and `inc bx` to increment a pointer. (Or to optimize for code size, use `cld` outside the loop, and have the caller pass a pointer in SI so you can use `lodsb` to load AL and increment SI.) – Peter Cordes Jun 08 '21 at 22:19
  • TL:DR: normally a function takes a pointer arg as one register, not as "DS: BX+SI" being a pointer to the characters. Also, you can set AH once outside the loop: `int 0x10 / ah=0x0e` doesn't modify AH or AL. You already depend on it not modifying FLAGS! (Normally you'd put cmp next to jne. And normally you'd want to not actually print the terminating `0` byte. e.g. put the load / cmp/jne at the bottom, and print at the top. You can enter the loop with a `jmp` to the load + loop condition. See [Why are loops always compiled into "do...while" style?](//stackoverflow.com/q/47783926)) – Peter Cordes Jun 09 '21 at 00:12