7

I'm writing some procedures in x86 assembler that modify ZF as a means to return boolean values, so I can do something like this:

call is_value_correct
jz not_correct

I'm wondering if this is considered bad practice since some coding standards say that simple values should be returned in the AX register.

Trap
  • 12,050
  • 15
  • 55
  • 67
  • 9
    If you don't have to follow a calling convention, and the callers are written in assembly, then I think it's perfectly acceptable. BIOS services did it a lot and I found it very useful (they spared me a test). – Margaret Bloom Jan 22 '18 at 12:11
  • 4
    no, it's not bad practice. A while ago even OSes (e.g. CP/M or DOS) used to return errors in flags ( e.g. DOS 21h function 03ch indicates an error in the CarryFlag) – Tommylee2k Jan 22 '18 at 12:42
  • 1
    as long you just call this function inside your code it is fine, libary functions are usually programmed in and for high-level-languages, where you usually have only one return-value usually in eax/rax. You could use the carry-flag too. – sivizius Jan 22 '18 at 13:29
  • 3
    `gcc` inline assembly can use flag registers to return values (since gcc 6 I believe). – EOF Jan 22 '18 at 17:54
  • 1
    @EOF although with slightly less efficiency you could do it manually in earlier GCCs by setting an output constraint to the value of one of the flags. – Michael Petch Jan 22 '18 at 20:18
  • 3
    It is fine. I've used this many times across many assembler languages over the last 50 years. The key point is to *document* the API carefully and accurately to reflect what registers/condition bits return in what state. – Ira Baxter Feb 24 '22 at 07:32

1 Answers1

16

Do it if it makes your code run faster and/or be smaller overall. It's not bad practice.

One of the advantages of writing in asm by hand is being able to use custom calling conventions functions, even when they're not private helper functions or macros. This includes being able to "return" multiple values in different registers, and basically do whatever you want.

As with any custom calling convention, all you need to do is document it with comments. Here's an example of how you might write such comments, with a specific and intentionally non-standard set of things.

# inputs:   foo in EAX (zero-extended into RAX), bar in EDI
# pre-requisites: DF=0
# clobbers: RCX, RDX, AL (but not the rest of EAX)
# returns:  AL = something,  ZF = something else
my_func:
   ...
   setc al
   ...
   something that sets ZF
   ret

If you're willing to sacrifice efficiency for style or readability, you probably shouldn't be writing in asm in the first place in 2018 when compilers are capable of generating good asm most of the time, and you rarely need to write your own boot sector or whatever. (i.e. performance is the main use-case left for hand-written asm, and it's only appropriate if you're going all out for performance.)

Yes it's possible for hand-written asm to become an unreadable / unmaintainable mess, but if done carefully when it has a reasonable semantic meaning, this optimization won't make your code a mess.


There is even precedent for doing this: x86-64 OS X system calls use CF as the error/no-error status, separate from the rax return value. Unlike Linux where errors are indicated by RAX return values from -4095 to -1, although otherwise they use the same x86-64 System V ABI / calling convention.

Some DOS int 0x21 system calls and PC BIOS int 0x10 functions have a similar flag-return system. It lets the caller branch on error, so it saves code-size (a test or cmp) and avoids needing in-band signaling of errors.


Inline asm

BTW, in inline assembly, not writing a whole function, you can and should output something in FLAGS instead of wasting instructions materializing a boolean in a register, especially if the compiler is just going to test it again. Condition-code output constraints are supported since GCC6, see this answer for an example.

But it's not easy to safely call a function from inside an asm statement if you want to use C to call a function with a custom calling convention. You need to tell the compiler that every call-clobbered register is clobbered. Including st0..7, mm0..7, x/y/zmm0..31, and k0..7 (if AVX-512 is supported, otherwise the compiler won't even know those reg names). Or only the ones your hand-written asm function actually clobbers, if you want to commit to maintaining the constraints in sync with the function's actual implementation.

And call itself does a push which steps on the red zone. See Calling printf in extended inline ASM for an example that should be safe.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 2
    I wonder why ABIs simply aren't such that returning `_Bool` is done via setting a flag. Using a register for `_Bool` returns seems like a waste to me. – Petr Skocik May 27 '19 at 12:25
  • 3
    @PSkocik: That would be interesting. Many functions need to `add` or `sub` to clean up a stack frame, but they could LEA. Or defer an actual flag-setting operation until right before returning, after running destructors. But the biggest problem is: which flag would you pick? CF is the obvious choice (because it has special instructions to use it, like `adc` / `sbb`), and some special instructions set it (`bt` / `bts` etc, `rdrand`). But if your `_Bool` comes from ZF from an instruction like `bsr` or `bsr` or an `==` compare, or SF + OF signed compare, you prob. need sete/le + cmp. – Peter Cordes May 27 '19 at 17:10
  • I'd like to point out that as of GCC 6.1 (April 2016), it's possible to directly return a value in the flags register [from inline assembly](https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#FlagOutputOperands). Which I know thanks to [this answer](https://stackoverflow.com/a/41668986/161468). – Deadcode Feb 23 '22 at 17:22
  • @Deadcode: yes, true, but this question is about whole functions, not inline asm. If you do have a hand-written function that ends with `ret` that doesn't follow the standard calling convention exactly, it's still a big pain to call it from C. If you use `asm("call foo" : "=@ccc", "+D"(arg))` you still need to declare clobbers on every call-clobbered register (including x/y/zmm0..31), and compile with `-mno-redzone` or otherwise work around it. So yeah, if you're using inline asm, probably better to transplant the actual important part into inline asm, not wrap a `call` – Peter Cordes Feb 23 '22 at 22:15
  • Not sure why you're talking about wrapping a `call`; I never suggested that. I've been [trying](https://codegolf.stackexchange.com/questions/181128/is-it-a-cyclops-number-nobody-knows/181158#comment513501_181202) to [spread](https://codegolf.stackexchange.com/questions/89436/is-it-a-proth-number/194905#comment516101_194905) the word but some still [don't know about it](https://codegolf.stackexchange.com/revisions/242996/5). I thought maybe you didn't know (you didn't mention it in that thread). You reply in asm threads more often than I; perhaps you too could tell people of this feature. – Deadcode Feb 24 '22 at 01:19
  • @Deadcode: This Q&A is about stand-alone assembly language functions / procedures. Called by other hand-written asm, not from C. C (and thus *inline* asm) doesn't enter into it at all. If there isn't a call/ret, it's just a macro, or something manually inlined into an asm function, and then it's fairly obvious that there's no point materializing a boolean into a register and then testing it again to get it back into FLAGS. There is no surrounding C code in this Q&A. – Peter Cordes Feb 24 '22 at 01:25
  • @Deadcode: As for code golf, the language of the answers you're commenting on is x86 **machine code**, not even assembly language. That's why we count machine-code bytes, not asm source size. Code-golf rules require functions or whole programs, *not* just snippets. Omitting a `ret` would make it not a function, just a snippet which you could describe via GNU C inline asm constraints. If you want the C function containing that asm snippet to be your code-golf answer, the language would be GNU C, not machine code, so your "IsCyclopsNumber" TIO link would be a 535 byte answer before golfing. – Peter Cordes Feb 24 '22 at 01:29
  • @Deadcode: Your C test harness using `asm("call bin\n\t" :"=@ccz" (r), ... )` on [Is it a fibonacci-like sequence?](https://codegolf.stackexchange.com/posts/comments/547762) is a good use-case for flag-output constraints, though. (Although I would have put the asm source into a global-scope `asm("testfib:\n" "dec ecx\n" ...` statement instead of using a hex-string to define an array of machine code; it's not like that gets TIO to count the actual size, and isn't easily editable.) But anyway, then it's what I was talking about: a C wrapper for a custom asm calling convention. – Peter Cordes Feb 24 '22 at 01:35
  • My "IsCyclopsNumber" TIO link was not doing anything the author's wasn't already doing; their TIO link also effectively encoded the `ret` in C rather than asm. When I'm changing somebody's TIO to illustrate a point, I generally keep the changing of unrelated things to a minimum, as changing more would potentially confuse the matter. That's why I also kept the `call` in my modification of the [Is it a fibonacci-like sequence?](https://codegolf.stackexchange.com/posts/comments/547762) TIO. – Deadcode Feb 24 '22 at 01:39
  • Regarding "This Q&A is about stand-alone assembly language functions / procedures": If you want me to delete my replies here then I'm willing to; I simply thought it was a useful place to put the information, as it's likely someone to whom it would be useful might run across it. If you disagree though, then delete your replies I'll go ahead and delete mine. BTW, are you the same Peter Cordes I originally met back in #calt-ti / #ticalc on EFnet IRC when I was going by the name eXocomp? – Deadcode Feb 24 '22 at 01:40
  • @Deadcode: Ok, yes, agreed condition-code output constraints are good for C wrappers for testing machine-code answers that use custom calling conventions. But that always involves a `call` inside the asm statement, which you said you were *not* talking about. OTOH I'd usually have to google the syntax, vs. being able to type a `setcc` easily, and efficiency is basically irrelevant for a test harness like that. – Peter Cordes Feb 24 '22 at 01:44
  • @Deadcode: I don't think we need to delete these comments, though; maybe someone who ended up on this Q&A after searching actually did have inline asm in mind. Maybe that's reason enough to mention it somewhere in my answer, although then I'd have to clutter it with discussion of actually calling functions from inline asm statements... BTW, no, I'm not the same Peter Cordes from IRC; I never really got into IRC chatting. I prefer less ephemeral mediums like Stack Overflow where it seems more worthwhile to spend effort on writing well. – Peter Cordes Feb 24 '22 at 01:47
  • I see your point regarding `call` (that is after all what this question was about), and about test harness efficiency as well; I suppose I just like to spread information about useful features when I can, even if it's via test harnesses. FWIW I met the other Peter Cordes in the late 1990s. In any case, I quite admire your work on Stack Overflow sites. – Deadcode Feb 24 '22 at 02:14
  • @Deadcode: Yeah, agreed about spreading useful information; that's what convinced me to add the new section at the bottom of my answer which is off topic for the actual question. – Peter Cordes Feb 24 '22 at 02:16