5
votes

I'm trying to learn GCC inline assembly on Linux (x86), and my first experiment was to try and implement integer overflow detection for multiplication. It seems easy enough, but it is having side effects which I don't understand.

So, here I want to multiply two unsigned 8-bit integers, and see if the result overflows. Basically I just load the first operand into the AL register and the other operand into the BL register, and then use the mul instruction. The result is stored as a 16-bit value in the AX register. So I then copy the value in the AX register to my C variable b, unless it overflows. If it overflows I set c to 1.

 uint8_t a = 10;
 uint8_t b = 25;
 uint8_t c = 0; // carry flag

 __asm__
 (
  "clc;"                  // Clear carry flag
  "movb %3, %%al;"        // Load b into %al
  "movb %2, %%bl;"        // Load a into %bl 
  "mul %%bl;"             // Multiply a * b (result is stored in %ax)
  "movw %%ax, %0;"        // Load result into b
  "jnc out;"              // Jump to 'out' if the carry flag is not set
  "movb $1, %1;"          // Set 'c' to 1 to indicate an overflow
  "out:"
  :"=m"(b), "=m"(c)       // Output list
  :"ir"(a), "m"(b)        // Input list
  :"%al", "%bl"           // Clobbered registers (not sure about this)
 );

This seems to work fine. If I printf the value of 'b' I get 250, which is correct. Also, if I change the starting value of 'b' to 26, then after the multiplication c is set to 1, indicating an overflow because of course (10 * 26 > ~uint8_t(0)). The problem I'm seeing is that the C variable a is set to 0 after the multiplication (or 1 on an overflow.) I don't understand why a would be changed at all by anything I'm doing here. It's not even on the list of output variables, so why is my assembly routine affecting the value of a?

Also, I'm unsure about the clobbered registers list. This list is supposed to inform GCC about any registers which were used during the assembly routine, so that GCC doesn't try to use them incorrectly. I think I need to inform GCC that I used the AL and BL registers, but what about the AX register? It's used implicitly to store the product of two 8-bit integers, so do I need to include it in the list of clobbered registers?

2
Instead of loading the values into %al and %bl and having to mark that they get clobbered, you should setup your constraints so that gcc puts the arguments in the right registers to begin with.R.. GitHub STOP HELPING ICE

2 Answers

7
votes

The problem I'm seeing is that the C variable a is set to 0 after the multiplication (or 1 on an overflow.) I don't understand why a would be changed at all by anything I'm doing here. It's not even on the list of output variables, so why is my assembly routine affecting the value of a?

mul %%bl multiplies AL (8 bits) by BL (8 bits), placing the result in AX (16 bits).

Note that AL and AX are not separate registers: AL is just the bottom 8 bits of AX.

movw %%ax, %0 stores AX (16 bits) to the address of b... which is a uint8_t. So this instruction is also overwriting the next byte in memory with the top 8 bits of the result. In this case, that byte happens to be where the value of a is stored (which explains why a is overwritten with 0 when it doesn't overflow, and 1 when it does).

You need to replace that with movb %%al, %0, to do just a byte store of the bottom 8 bits of the result.

I think I need to inform GCC that I used the AL and BL registers, but what about the AX register? It's used implicitly to store the product of two 8-bit integers, so do I need to include it in the list of clobbered registers?

Yes - you should tell GCC about any register that you change the value of (and, as nategoose pointed out in another answer, you should probably tell it that you're changing the flags as well). So the clobber list here should be "%ax", "%bl", "cc" (AX includes AL, so you don't need to mention AL explicitly).

2
votes

You should compile your code with the -S option and have a look at the *.s file. All of your assembly is on the same line separated by semicolons, and I believe that semicolons begin comments in gnu assembler. You'll need to add "\n" (or better yet "\n\t") to the ends of all of your assembly instructions.

You might also want to add "cc" to the clobber list.

Additionally, GCC has a way to specify that input is both an input and an output to assembly which you might be interested in.

Also, it is probably best if let GCC decide where the inputs to this are rather than forcing them be in memory. I seem to remember that GCC's inline assembly has a constraint on x86 that means "either in a register or in memory" that can be used for circumstances where it doesn't matter, but you probably shouldn't have many "mov" instructions at the beginning and ends of your inline assembly because one of GCC's biggest jobs is figuring out the best sets of move instructions to put between actual calculating instructions. For instance, in your code the best thing for GCC to do would be to just store the constants 10 and 25 in the registers that you are using to begin with.