call subroutines conditionally in assembly
Asked Answered
S

6

9

I'm learning x86 assembly. I was wondering how you perform call a subroutine conditionally. As far as I understand, jumping to a label doesn't work because the return address is not stored and therefore it does not know where to return.

 cmp bx, 0
 jz zero ; how do I do this correctly ?
 ; do something else and exit

 zero:
 ; do something
 ret
Skiplane answered 4/9, 2011 at 19:41 Comment(2)
I haven't written assembly in a while but I think I remember pushing addresses either on the stack or in a register and jumping to that value at the end of the subroutine.Hibernia
Related: What if there is no return statement in a CALLed block of code in assembly programs / JMP vs. CALL in 8086 assembly (and a simple but clearly-named Assembly: Why does jumping to a label that returns via ret cause a segmentation fault?). Also What if there is no return statement in a CALLed block of code in assembly programs)Sporadic
T
8

The clean way to do it is simply:

    cmp bx,0
    jnz notzero
    ; handle case for zero here
    jmp after_notzero
notzero:
    ; handle case for not zero here
after_notzero:
    ; continue with rest of processing

I know no better way for an if-else situation. Ok, if both branches must return directly afterward, you can do:

    cmp bx,0
    jnz notzero
    ; handle case for zero here
    ret

notzero:
    ; handle case for not zero here
    ret

If some processing must take place before the ret (e.g. popping values previously pushed), you should use the first approach.

Tragic answered 5/9, 2011 at 3:14 Comment(1)
You can do an if/else in asm with no taken branches on one of the paths. The conditional jump goes to a block after the ret in the function, so the fast-path doesn't need to jump over it. So the fast-path has one not-taken branch, and the slow-path has two taken branches. (It jumps back to wherever you want.) gcc makes code like this all the time: little blocks of code with one or more insns and then a jmp back up into the main part of the function. Good point that conditional tail-calls are possible. gcc doesn't, but couldSporadic
H
6

Well it works if you don't need to return to that address. Often times you can structure your code such that this is the case.

Otherwise you'll have to do the branching with Jxx instructions that jump around the call site or in other ways structure your code around this limitation. In this case inverting the test should work:

    cmp bx, 0
    jnz not_zero
    call zero
    ; fall through here, return or do what you want
not_zero:
    ; do something else and exit
    ; ...
    ret 

zero:
    ; do something
    ret

EDIT 2016-04-25: As @Peter Cordes mentions in the comments, the below code will probably perform terribly. See e.g. this article for an explanation why.

@Manny Ds suggestion in the comments inspired me to write the following. It might not be cleaner or better, but it's another way to structure it:

    push back_from_zero ; Push where we want to return after possibly branching to 'zero' 
    cmp bx, 0
    jz zero ; branch if bx==0
    add esp, 4 ; adjust stack in case we didn't branch
back_from_zero: ; returning from the zero branch here or continuing from above
    ; do something else and exit
    
zero:
    ; do something
    ret

It explicitly pushes the return address on the stack so the zero function can return or pops the value (add esp, 4) from the stack if we don't call the function (to readjust to stack). Note that you need to do some slight adjustments if you want this to work in either 16- or 64-bit mode.

Hospers answered 4/9, 2011 at 19:45 Comment(3)
Unfortunately, I have an if-else situation so I don't think I can restructure like that. I can probably have that bit of code in a separate subroutine just for that particular case, but it seems a bit hacky. I was hoping there was a clean way to do it.Skiplane
Never use the push back_from_zero trick if performance matters at all. Modern CPUs keep a return-address prediction stack, and a mis-matched call/ret breaks that stack, leading to a branch-mispredict on every ret for maybe the next 15 levels of returns. Avoid this by having the called function return manually (without ret): e.g. pop the return addr and jmp edx. Or in a 64bit ABI with a red-zone, you could also adjust the stack and then do a memory-indirect jmp [rsp - 8]. That indirect jmp probably won't have a BTB entry, but won't destroy performance for other code after.Sporadic
It's actually fine if zero ends with pop edx / jmp edx or something. But that only works if the function is only ever called without a call insn. If there's only one call site, Rudy's answer of how to properly structure the branches to conditionally run something is a much better approach.Sporadic
S
2

I believe the right way to do it is with the call instruction. This is equivalent to a function call in a higher programming language. The PC is stored on the stack, and therefore the ret at the end of your zero: subroutine does what it's supposed to.

Sweepstake answered 4/9, 2011 at 19:46 Comment(2)
I'm aware of the call instruction but I don't know the syntax to call it based on a condition.Skiplane
The "PC" is called "eip" on the x86 ;)Locution
D
2

... how you perform call a subroutine conditionally. As far as I understand, jumping to a label doesn't work because the return address is not stored and therefore it does not know where to return.

The x86 call instruction doesn't operate conditionally. In essence the call instruction jumps to a subroutine and the ret instruction there jumps back from the subroutine.
Now an x86 jmp can operate conditionally. So if we spent a register (preferably a call-preserved one) to hold the return address we can easily execute both these jumps.

The subroutine

subroutine:
  push bp               ; Only if BP is used locally

  ...

  pop  bp               ; Only if BP is used locally
  jmp  bp

Call site with an ELSE clause

  ; IF condition THEN
  ;   GOSUB subroutine
  ; ELSE
  ;
  ;   ...
  ;
  ; ENDIF

  mov  bp, back
  test bx, bx
  jz   subroutine       ; Jump to the subroutine

  ...

back:                   ; The subroutine jumps back to here

Call site without an ELSE clause

  ; IF condition THEN
  ;   GOSUB subroutine
  ; ENDIF

  mov  bp, back
  test bx, bx
  jz   subroutine       ; Jump to the subroutine
back:                   ; The subroutine jumps back to here
Dignify answered 24/3 at 21:4 Comment(1)
Note that this manual return-address-register trick is usually not the most efficient way to do this. It's sort of closest to what's literally being asked for, but usually you just want to branch over a call instruction if you can't just do a conditional tail-call.Sporadic
W
-2

if I understand correctly what you are looking for is for example if CMP yields zero then make a CALL instead of JMP. one workaround is using a temp subfunction to call the intended function

example: below program at #4 you do CMP then Jz to (made_up) label, then use that (made_up) label to call the actual function you want.

    #1 instruction xxx,xxx
    #2 instruction xxx,xxx
    #3 instruction xxx,xxx
    #4 CMP XXX , XXX 
    #5 JZ Made_up
    #6 Made_up:
    #7  call your_function
    #8 next instruction address will be pushed to stack as your return address.


    cmp xxx, xxx
    jz Made_up

    Made_UP:
        call your_Function
    ;now you will have the return address saved onto the stack to return to.

hope this helps...

Wattage answered 30/3, 2023 at 5:18 Comment(1)
Execution reaches Made_up: whether or not the jz is taken. Put the call somewhere that won't be reached if the branch is not taken, not like you've shown where it's the next instruction so will be reached by either fall-through or taken cases. Or as shown in other answers, jump forward over a call, with the opposite branch condition (jnz).Sporadic
S
-2

This code its works for me:

example:

.model small
.stack 100h
.data

address dw 0 ;variable to store the address

.code   
mov ax,@data
mov ds,ax
    
mov bx,0 

;Here is the important code--------
call get_address 
cmp bx,0 
jz  zero 
; do something else and exit   
    
zero:  
   ; do something     
   jmp address ;instead this line you could use the following code 
   ; mov ax, address  
   ; push ax
   ; ret

get_address PROC
    pop ax 
    mov address,ax
    add address,7 
    jmp ax ; instead this line you could use the following code
   ; push ax 
   ; ret
get_address ENDP
Saintjust answered 15/3 at 1:26 Comment(5)
zero is the function that's supposed to be called if bx is 0. This jumps there without a return address on the stack. That's the problem this question is trying to work around. After your last edit, that function now pops the stack and lets execution fall through out the bottom into whatever's next. That's nothing like a function call, and not even a tailcall.Sporadic
Sorry this is my first answer, I fixed the code, I'm sure it works now.Saintjust
You fixed the xor ax typo for xor ax,ax. You didn't fix the branching logic; try single-stepping it in a debugger in a minimal test program and see what happens. See What if there is no return statement in a CALLed block of code in assembly programs . If your stack pointer is currently pointing at a return address, jz zero is a valid way to make a tailcall, but then zero proc should still end with ret. It can't possibly be correct for it to end with pop ax and fall through off the bottom of the function.Sporadic
This is just a way to do it without using tags to store the memory address. It works on my machine, I'll delete the answer if it doesn't work on others.Saintjust
This new version should work, although it has to hard-code the machine-code size of the cmp bx, 0 / jz zero (which should only be 5 bytes total in 16-bit code: opcode + modrm + imm8 for the cmp since you used that instead of 2-byte test bx,bx, and 2-byte jcc rel8. If you assembled for 32-bit mode, then it would be 7, with operand-size prefixes on each). It's not efficient compared to just jnz skip_call to jump forward over a call zero instruction, and with mismatched call / ret from the call get_address it won't perform well on modern CPUs.Sporadic

© 2022 - 2024 — McMap. All rights reserved.