0

I'm trying to pass arguments to a function call by pushing them onto the stack, then popping them into registers inside the function like this:

    push keystring+0x7c00
    call PrintString
    jmp $

;====functions====

PrintString:
    pop ecx
    mov ah, 0x0e
LoopHead:
    mov bl, 0
    mov al, [ecx]
    int 0x10
    inc ecx
    add bl, [ecx]
    jnz LoopHead
    ret

;====variables====

keystring:
    db "Press any key...", 0

;====padding====

    times 510-($-$$) db 0
    db 0x55, 0xAA


It works fine if I do it by storing the argument in ecx and using ecx in the function, but for some reason pop just won't work. Does anybody know how I can fix this?

Humanagon
  • 1
  • 4
  • 1
    Call instruction pushes the returning address on the top of the stack. Your argument is then no longer at the top of the stack. Also, you need **RET 4** instead of **RET** – RCECoder May 18 '23 at 08:28
  • @RCECoder I tried popping twice though, and it only displayed a single capital S, even though there is no capital s in the string anywhere. – Humanagon May 18 '23 at 08:32
  • 1
    @RCECoder: you don't necessarily need `ret 4`; caller-pops is a valid (and common) convention. – Peter Cordes May 18 '23 at 08:32
  • 1
    @Humanagon: You're loading this as an MBR boot sector, building it with `nasm -fbin`? So you're assembling for 16-bit mode. `push` will I think assemble as only a `push imm16` push of a 16-bit address. (And `call` will certainly push a 16-bit return address) But you're popping 4 bytes. – Peter Cordes May 18 '23 at 08:35
  • 1
    Also, instead of `keystring+0x7c00`, you'd normally use `org 0x7c00` at the top of your source. Also, you should set `DS` to zero to match that ORG setting; some BIOSes will start an MBR with DS=07C0. – Peter Cordes May 18 '23 at 08:36
  • @PeterCordes That's right like cdecl calling convention but I meant in this question. – RCECoder May 18 '23 at 08:38
  • @PeterCordes yeah I tried popping it into cx, but it wouldn't let me use it as an address. It said: boot.asm:12: error: invalid effective address Also, if I tried zeroing ecx, popping into cx, then finally using ecx as the address, it just gave garbage characters. – Humanagon May 18 '23 at 08:40
  • @RCECoder: What in this question do you see that implies callee-pops? The caller just infinite loops, which you'd do with either convention. From the fact that they want to pop the stack to access args, instead of `mov di, [esp + 2]` (assuming SP is correctly zero-extended to ESP), or like normal 16-bit code set up BP as a frame pointer and `mov di, [bp + 4]`. To be able to `ret` at all, they SP to be pointing to a valid return address, if they `pop cx` / `pop di` then can return with the arg already popped by `push cx` / `ret`. Without a push at all, `ret 4` or `ret 2` would pop garbage. – Peter Cordes May 18 '23 at 08:44
  • 1
    @Humanagon: Right, that's one way to zero-extend 16 bits into a 32-bit register. If you're going to write legacy 16-bit code, you're going to need to spend extra time learning the quirks of 16-bit mode. I'd recommend learning normal 32-bit code under an OS with a convenient debugger before you mess around with this, but if you really want, see [Differences between general purpose registers in 8086: \[bx\] works, \[cx\] doesn't?](https://stackoverflow.com/q/53866854). Using a debugger will save tons of time; the one built-in to Bochs is good for real mode and bootloaders. – Peter Cordes May 18 '23 at 08:47

1 Answers1

0

I figured it out guys, I had to pop it into cx twice. I couldn't of figured it out without y'alls suggestions though, like trying 16 bit addresses, and knowing that the return address gets pushed to the stack automatically. Here's the fixed code if anyone's interested:

    org 0x7c00
    push keystring
    call PrintString
    jmp $

;====functions====

PrintString:
    pop di
    shl edi, 16
    pop di
    push eax
    push ebx
    push ecx
    xor ecx, ecx
    mov cx, di
    mov ah, 0x0e
LoopHead:
    mov al, [ecx]
    int 0x10
    inc ecx
    mov bl, [ecx]
    add bl, 0
    jnz LoopHead
    pop ecx
    pop ebx
    pop eax
    shr edi, 16
    push di
    ret

;====variables====

keystring:
    db "Press any key...", 0

;====padding====

    times 510-($-$$) db 0
    db 0x55, 0xAA
Humanagon
  • 1
  • 4
  • This way the `retn` won't go where it is supposed to. – ecm May 18 '23 at 08:51
  • 1
    Remaining bugs: your `ret` pops garbage into IP, very unlikely that it returns to the `jmp $` that follows the `call`. You popped the return address into CX then overwrote it, and didn't even try to point SP at it before running a `ret`. What you're doing can only work if you don't want to ret. Also, you don't set `ds`, so this will only work on some BIOSes. Both of these problems were discussed in comments. – Peter Cordes May 18 '23 at 08:51
  • 1
    There are various examples on Stack Overflow of writing bootloader code, and passing args to 16-bit functions via the stack if you don't want to pass more efficiently in a register. (e.g. to learn how it's done in case you wanted to write a function with many args.) e.g. [x86 assembly: Pass parameter to a function through stack](https://stackoverflow.com/q/37511492) – Peter Cordes May 18 '23 at 08:53
  • @ecm you're right, I fixed that just now – Humanagon May 18 '23 at 08:55
  • Your function is seriously over-complicated. If you want to be using 32-bit address-size, don't write 16-bit code in the first place. You're already popping into `di`, and `[di]` is a valid 16-bit addressing mode, so zero-extending into ECX is totally unnecessary. Or if you do want to do that, use `movzx ecx, di` since you're writing code that will only run on a 386 or later, so 386 new instructions like `movzx` are available. Also, saving/restoring all of ECX, EBX, and EAX is weird when you're clobbering EDI. (In 32-bit calling conventions, EAX, ECX, and EDX are usually call-clobbered) – Peter Cordes May 18 '23 at 12:14
  • 1
    If I wanted to do this weird hack of popping the return address and first arg, I'd probably do `pop edi` / `push edi` (to load + restore the return address and first arg) then `shr edi, 16` and use `[di]` inside the function. (I could have used `rol edi, 16` to swap halves, and still used `[di]` without disturbing it, but restoring the return address right away is cheaper than ending with `shr edi, 16` / `push di` / `ret`.) – Peter Cordes May 18 '23 at 12:18
  • Or even `pop edi` / `push di` if I wanted to emulate ending with a `ret 2`, to return with the caller's push already undone. – Peter Cordes May 18 '23 at 22:38