Comparing C source code with its machine-level translation is an invaluable exercise for any developer or security researcher. Compilation is not a mere literal translation; it is an optimization process where the compiler often prioritizes portability and robustness over the use of direct, specialized processor instructions. To illustrate this, we are going to use a simple program which will be compiled for a MIPS processor:
#include <stdio.h>
#include <math.h>
void test_floating_point(float f) {
// C'est ici qu'on verra si le compilateur génère 'abs.s'
float res = fabsf(f);
printf("FABS of %f is %f\n", f, res);
}
int main() {
test_floating_point(-3.14159f);
return 0;
}
The commands to compile this program are the following
mips-linux-gnu-gcc -g -fno-stack-protector -z execstack -no-pie -march=mips32 -mabi=32 -o "$OUT_MIPS/tests/mips1" "$SRC/mips1.c"
mips-linux-gnu-objdump -d --show-raw-insn -M reg-names=32 "$OUT_MIPS/tests/mips1" > "$OUT_MIPS/tests/mips1.mips"
mips-linux-gnu-readelf -a "$OUT_MIPS/tests/mips1" > "$OUT_MIPS/tests/mips1.mips_elf"
and below the assembly
00400650 <test_floating_point>:
400650: 27bdffc8 addiu sp,sp,-56
400654: afbf0034 sw ra,52(sp)
400658: afbe0030 sw s8,48(sp)
40065c: 03a0f025 move s8,sp
400660: 3c1c0043 lui gp,0x43
400664: 279c8010 addiu gp,gp,-32752
400668: afbc0018 sw gp,24(sp)
40066c: e7cc0038 swc1 fa0,56(s8)
400670: 8fc30038 lw v1,56(s8)
400674: 3c027fff lui v0,0x7fff
400678: 3442ffff ori v0,v0,0xffff
40067c: 00621024 and v0,v1,v0
400680: afc20020 sw v0,32(s8)
400684: c7c00038 lwc1 fv0,56(s8)
400688: 460000a1 cvt.d.s fv1,fv0
40068c: c7c00020 lwc1 fv0,32(s8)
400690: 46000021 cvt.d.s fv0,fv0
400694: f7a00010 sdc1 fv0,16(sp)
400698: f7c20028 sdc1 fv1,40(s8)
40069c: 8fc7002c lw a3,44(s8)
4006a0: 8fc60028 lw a2,40(s8)
4006a4: 3c020040 lui v0,0x40
4006a8: 24440790 addiu a0,v0,1936
4006ac: 8f828024 lw v0,-32732(gp)
4006b0: 0040c825 move t9,v0
4006b4: 0320f809 jalr t9
4006b8: 00000000 nop
4006bc: 8fdc0018 lw gp,24(s8)
4006c0: 00000000 nop
4006c4: 03c0e825 move sp,s8
4006c8: 8fbf0034 lw ra,52(sp)
4006cc: 8fbe0030 lw s8,48(sp)
4006d0: 27bd0038 addiu sp,sp,56
4006d4: 03e00008 jr ra
4006d8: 00000000 nop
004006dc <main>:
4006dc: 27bdffe0 addiu sp,sp,-32
4006e0: afbf001c sw ra,28(sp)
4006e4: afbe0018 sw s8,24(sp)
4006e8: 03a0f025 move s8,sp
4006ec: 3c020040 lui v0,0x40
4006f0: c44007a4 lwc1 fv0,1956(v0)
4006f4: 46000306 mov.s fa0,fv0
4006f8: 0c100194 jal 400650 <test_floating_point>
4006fc: 00000000 nop
400700: 00001025 move v0,zero
400704: 03c0e825 move sp,s8
400708: 8fbf001c lw ra,28(sp)
40070c: 8fbe0018 lw s8,24(sp)
400710: 27bd0020 addiu sp,sp,32
400714: 03e00008 jr ra
400718: 00000000 nop
40071c: 00000000 nop
Disassembly of section .MIPS.stubs:
00400720 <_MIPS_STUBS_>:
400720: 8f998010 lw t9,-32752(gp)
400724: 03e07825 move t7,ra
400728: 0320f809 jalr t9
40072c: 24180009 li t8,9
400730: 8f998010 lw t9,-32752(gp)
400734: 03e07825 move t7,ra
400738: 0320f809 jalr t9
40073c: 24180007 li t8,7
...
and a part of the elf
Global entries
| Address | Access | Initial | Sym.Val. | Type | Ndx | Name |
|---|---|---|---|---|---|---|
| 0042002c | -32740(gp) | 00000000 | 00000000 | NOTYPE | UND | _ITM_registerTMCloneTable |
| 00420030 | -32736(gp) | 00000000 | 00000000 | FUNC | UND | gmon_start |
| 00420034 | -32732(gp) | 00400730 | 00400730 | FUNC | UND | printf |
| 00420038 | -32728(gp) | 00000000 | 00000000 | NOTYPE | UND | _ITM_deregisterTMCloneTable |
| 0042003c | -32724(gp) | 00400720 | 00400720 | FUNC | UND | __libc_start_main |
1. why it is interesting ?
To understand, you have to know that the MIPS® Architecture For Programmers Volume II-A which is the reference for the instructions set of the mips processor contains very specific instructions to calculate the absolute value of a floating point. In fact it contains 3 specific instructions
ABS.S
ABS.D
ABS.PS
so we should find one of these instructions in the test_floating_point function. we don't find any of these instructions, instead we find
400674: 3c027fff lui v0,0x7fff # v0 = 0x7fff0000
400678: 3442ffff ori v0,v0,0xffff # v0 = 0x7fffffff
40067c: 00621024 and v0,v1,v0 # v0 = v1 & 0x7fffffff
The compiler applied a fundamental property of the IEEE 754 format here: a 32-bit floating-point number consists of one sign bit, eight exponent bits, and 23 mantissa bits. To calculate the absolute value, it is sufficient to force the sign bit to 0. This is exactly what the AND operation with 0x7FFFFFFF performs.
As seen with this example, the compiler often generates code compatible with multiple FPU variants, or even "Soft-Float" modes, to avoid creating overly strict dependencies on specific hardware.
2. Epilogue
In this very short article we explained a very specific point of what gcc does when it compiles a C source code. This shows why disassembling a binary compiled with gcc is always interesting. As a reverser, this analysis teaches us a crucial lesson: never rely solely on an architecture's theoretical documentation. Real code is the result of a chemistry between the processor manual, the ABI (Application Binary Interface) rules, and the compiler's optimization choices.

Top comments (0)