We can see all of the relevant combinations of instructions with the following block of code:
if ( ia ) { printf("x\n"); } else { printf("y\n"); }
if ( ca ) { printf("x\n"); } else { printf("y\n"); }
if ( la ) { printf("x\n"); } else { printf("y\n"); }
if ( ca == 0 ) { printf("x\n"); } else { printf("y\n"); }
if ( uca == 0 ) { printf("x\n"); } else { printf("y\n"); }
if ( ia == ib ) { printf("x\n"); } else { printf("y\n"); }
if ( ca == cb ) { printf("x\n"); } else { printf("y\n"); }
if ( uca == ucb ) { printf("x\n"); } else { printf("y\n"); }
if ( la == lb ) { printf("x\n"); } else { printf("y\n"); }
if ( ia == 2 ) { printf("x\n"); } else { printf("y\n"); }
if ( ca == 2 ) { printf("x\n"); } else { printf("y\n"); }
if ( uca == 2 ) { printf("x\n"); } else { printf("y\n"); }
if ( la == 2 ) { printf("x\n"); } else { printf("y\n"); }
if ( 2 == ia ) { printf("x\n"); } else { printf("y\n"); }
if ( 2 == ca ) { printf("x\n"); } else { printf("y\n"); }
if ( 2 == uca ) { printf("x\n"); } else { printf("y\n"); }
if ( 2 == la ) { printf("x\n"); } else { printf("y\n"); }
if ( 0 == ia ) { printf("x\n"); } else { printf("y\n"); }
if ( 0 == ca ) { printf("x\n"); } else { printf("y\n"); }
if ( 0 == uca ) { printf("x\n"); } else { printf("y\n"); }
if ( 0 == la ) { printf("x\n"); } else { printf("y\n"); }
For brevity, in the assembly we will only show the
printf
calls and else
clause for the first
case.
The equivalent assembly is shown in the following tables, organized by related statements:
C | .s file | gdb |
---|---|---|
if ( ia ) { |
||
cmpl $0, -8(%rbp) |
cmpl $0x0,-0x8(%rbp) |
|
je LBB0_2 |
je 0x100000a1d |
|
printf("x\n"); |
||
leaq L_.str(%rip), %rdi |
lea 0x58e(%rip),%rdi |
|
movb $0, %al |
mov $0x0,%al |
|
callq _printf |
call 0x100000f9c |
|
} |
||
jmp LBB0_3 |
jmp 0x100000a2b |
|
else { |
||
LBB0_2: |
||
printf("y\n"); |
||
leaq L_.str.1(%rip), %rdi |
lea 0x581(%rip),%rdi |
|
movb $0, %al |
mov $0x0,%al |
|
callq _printf |
call 0x100000f9c |
|
} |
||
LBB0_3: |
||
if ( ca ) { |
||
cmpb $0, -29(%rbp) |
cmpb $0x0,-0x1d(%rbp) |
|
je LBB0_8 |
je 0x100000a65 |
|
if ( la ) { |
||
cmpq $0, -48(%rbp) |
cmpq $0x0,-0x30(%rbp) |
|
je LBB0_14 |
je 0x100000aae |
|
if ( ca == 0 ) { |
||
movsbl -29(%rbp), %eax |
movsbl -0x1d(%rbp),%eax |
|
cmpl $0, %eax |
cmp $0x0,%eax |
|
jne LBB0_62 |
jne 0x100000d12 |
|
if ( uca == 0 ) { |
||
movzbl -32(%rbp), %eax |
movzbl -0x20(%rbp),%eax |
|
cmpl $0, %eax |
cmp $0x0,%eax |
|
jne LBB0_65 |
jne 0x100000d39 |
Here, we see that evaluating an integer as a boolean expression is
equivalent to comparing with 0. The comparison instruction is
cmpl
, cmpb
, or cmpq
. This tests
for equality, and is typically followed by a jump instruction
(je
for “jump if equal” or jne
for “jump if
not equal”).
The argument to the jump instruction is a label in the assembly code,
which when run becomes the address of the target instruction. Because of
the else
clause, the first block ends with a jump
(jmp
) to the instruction after the if/else statement. The
gcc compiler is generating slightly different code for
if (ia)
than if (ia == 0)
, choosing a
je
for the former and jne
for the latter,
reversing the true and false blocks.
To call a function, we set up the arguments to the function, and then
execute the callq
instruction. In the assembly, we specify
the symbol for the target function, and when run this is replaced with
the address of the first instruction of the function. This is similar to
what we saw for jmp
in the conditional.
The instructions preceding callq
will vary greatly, but
it is worth taking a quick look at the leaq
(load effective
address) instruction. While movl -8(%rbp), -12(%rbp)
would
move the value of ia
to ib
, the instruction
leaq -8(%rbp), -12(%rbp)
would move the address
rbp-8 to ib
.
C | .s file | gdb |
---|---|---|
if ( ia == ib ) { |
||
movl -8(%rbp), %eax |
mov -0x8(%rbp),%eax |
|
cmpl -12(%rbp), %eax |
cmp -0xc(%rbp),%eax |
|
jne LBB0_20 |
jne 0x100000af9 |
|
if ( ca == cb ) { |
||
movsbl -29(%rbp), %eax |
movsbl -0x1d(%rbp),%eax |
|
movsbl -30(%rbp), %ecx |
movsbl -0x1e(%rbp),%ecx |
|
cmpl %ecx, %eax |
cmp %ecx,%eax |
|
jne LBB0_26 |
jne 0x100000b49 |
|
if ( uca == ucb ) { |
||
movzbl -32(%rbp), %eax |
movzbl -0x20(%rbp),%eax |
|
movzbl -33(%rbp), %ecx |
movzbl -0x21(%rbp),%ecx |
|
cmpl %ecx, %eax |
cmp %ecx,%eax |
|
jne LBB0_29 |
jne 0x100000b73 |
|
if ( la == lb ) { |
||
movq -48(%rbp), %rax |
mov -0x30(%rbp),%rax |
|
cmpq -56(%rbp), %rax |
cmp -0x38(%rbp),%rax |
|
jne LBB0_32 |
jne 0x100000b9b |
For variable comparisons, we see familiar patterns. One or both variables is loaded into a register, and then we do a comparison and conditional jump as in the previous block of statements.
C | .s file | gdb |
---|---|---|
if ( ia == 2 ) { |
||
cmpl $2, -8(%rbp) |
cmpl $0x2,-0x8(%rbp) |
|
jne LBB0_38 |
jne 0x100000be7 |
|
if ( ca == 2 ) { |
||
movsbl -29(%rbp), %eax |
movsbl -0x1d(%rbp),%eax |
|
cmpl $2, %eax |
cmp $0x2,%eax |
|
jne LBB0_44 |
jne 0x100000c32 |
|
if ( uca == 2 ) { |
||
movzbl -32(%rbp), %eax |
movzbl -0x20(%rbp),%eax |
|
cmpl $2, %eax |
cmp $0x2,%eax |
|
jne LBB0_47 |
jne 0x100000c59 |
|
if ( la == 2 ) { |
||
cmpq $2, -48(%rbp) |
cmpq $0x2,-0x30(%rbp) |
|
jne LBB0_50 |
jne 0x100000c7e |
|
if ( 2 == ia ) { |
||
movl $2, %eax |
mov $0x2,%eax |
|
cmpl -8(%rbp), %eax |
cmp -0x8(%rbp),%eax |
|
jne LBB0_74 |
jne 0x100000dab |
|
if ( 2 == ca ) { |
||
movsbl -29(%rbp), %ecx |
movsbl -0x1d(%rbp),%ecx |
|
movl $2, %eax |
mov $0x2,%eax |
|
cmpl %ecx, %eax |
cmp %ecx,%eax |
|
jne LBB0_80 |
jne 0x100000dfe |
|
if ( 2 == uca ) { |
||
movzbl -32(%rbp), %ecx |
movzbl -0x20(%rbp),%ecx |
|
movl $2, %eax |
mov $0x2,%eax |
|
cmpl %ecx, %eax |
cmp %ecx,%eax |
|
jne LBB0_83 |
jne 0x100000e29 |
|
if ( 2 == la ) { |
||
movl $2, %eax |
mov $0x2,%eax |
|
cmpq -48(%rbp), %rax |
cmp -0x30(%rbp),%rax |
|
jne LBB0_86 |
jne 0x100000e52 |
Here we see that comparing a variable with a literal integer value,
we have very similar code to when we were doing a comparison with 0. The
order in which we compare (ia == 2
vs 2 == ia
)
produces the same assembly.
C | .s file | gdb |
---|---|---|
if ( 0 == ia ) { |
||
xorl %eax, %eax |
xor %eax,%eax |
|
cmpl -8(%rbp), %eax |
cmp -0x8(%rbp),%eax |
|
jne LBB0_92 |
jne 0x100000ea0 |
|
if ( 0 == ca ) { |
||
movsbl -29(%rbp), %ecx |
movsbl -0x1d(%rbp),%ecx |
|
xorl %eax, %eax |
xor %eax,%eax |
|
cmpl %ecx, %eax |
cmp %ecx,%eax |
|
jne LBB0_98 |
jne 0x100000eed |
|
if ( 0 == uca ) { |
||
movzbl -32(%rbp), %ecx |
movzbl -0x20(%rbp),%ecx |
|
xorl %eax, %eax |
xor %eax,%eax |
|
cmpl %ecx, %eax |
cmp %ecx,%eax |
|
jne LBB0_101 |
jne 0x100000f15 |
|
if ( 0 == la ) { |
||
xorl %eax, %eax |
xor %eax,%eax |
|
cmpq -48(%rbp), %rax |
cmp -0x30(%rbp),%rax |
|
jne LBB0_104 |
jne 0x100000f3b |
The comparison 0 == ia
interestingly produces different
instructions. Instead of using a literal $0
, we instead use
xorl
to set the value of eax
to 0, and compare
with that. The result of the xor is stored in the second argument.
The equivalent assembly is shown in the following table:
C | .s file | gdb |
---|---|---|
if ( ia ) { |
||
ldr r3, [fp, #-8] |
ldr r3, [r11, #-8] |
|
cmp r3, #0 |
cmp r3, #0 |
|
beq .L2 |
beq 0x10890 |
|
printf("x\n"); |
||
ldr r0, .L75 |
ldr r0, [pc, #1220] |
|
bl puts |
bl 0x102e4 |
|
b .L3 |
b 0x10898 |
|
} |
||
else { |
||
.L2: |
||
printf("y\n"); |
||
ldr r0, .L75+4 |
ldr r0, [pc, #1212] |
|
bl puts |
bl 0x102e4 |
|
} |
||
.L3: |
||
if ( ca ) { |
||
ldrb r3, [fp, #-13] |
ldrb r3, [r11, #-13] |
|
cmp r3, #0 |
cmp r3, #0 |
|
beq .L6 |
beq 0x108d0 |
|
if ( la ) { |
||
ldr r3, [fp, #-20] |
ldr r3, [r11, #-20] |
|
cmp r3, #0 |
cmp r3, #0 |
|
beq .L10 |
beq 0x10910 |
|
if ( ca == 0 ) { |
||
ldrb r3, [fp, #-13] |
ldrb r3, [r11, #-13] |
|
cmp r3, #0 |
cmp r3, #0 |
|
bne .L42 |
bne 0x10b28 |
|
if ( uca == 0 ) { |
||
ldrb r3, [fp, #-14] |
ldrb r3, [r11, #-14] |
|
cmp r3, #0 |
cmp r3, #0 |
|
bne .L44 |
bne 0x10b48 |
For the comparisons, we see the same pattern as for AMD64: compare
with 0 and then either “branch-if-equal” (beq
) or
“branch-if-not-equal” (bne
), depending on whether we are
doing if(ia)
or if(ia==0)
. The main difference
is that, as is often the case, for AArch64 we first load (with
ldr
or ldrb
) the variable’s value into a
register.
When calling a function, we again prepare the stack, and then “branch
with link” (bl
) to the function, which is the equivalent of
callq
on AMD64. For the conditional flow, the “branch”
(b
) instruction is the equivalent of jmp
.
C | .s file | gdb |
---|---|---|
if ( ia == ib ) { |
||
ldr r2, [fp, #-8] |
ldr r2, [r11, #-8] |
|
ldr r3, [fp, #-28] |
ldr r3, [r11, #-28] |
|
cmp r2, r3 |
cmp r2, r3 |
|
bne .L14 |
bne 0x10954 |
|
if ( ca == cb ) { |
||
ldrb r2, [fp, #-13] |
ldrb r2, [r11, #-13] |
|
ldrb r3, [fp, #-33] |
ldrb r3, [r11, #-33] |
|
cmp r2, r3 |
cmp r2, r3 |
|
bne .L18 |
bne 0x1099c |
|
if ( uca == ucb ) { |
||
ldrb r2, [fp, #-14] |
ldrb r2, [r11, #-14] |
|
ldrb r3, [fp, #-34] |
ldrb r3, [r11, #-34] |
|
cmp r2, r3 |
cmp r2, r3 |
|
bne .L20 |
bne 0x109c0 |
|
if ( la == lb ) { |
||
ldr r2, [fp, #-20] |
ldr r2, [r11, #-20] |
|
ldr r3, [fp, #-40] |
ldr r3, [r11, #-40] |
|
cmp r2, r3 |
cmp r2, r3 |
|
bne .L22 |
bne 0x109e4 |
When comparing two integer variables, we see almost the same pattern,
but instead of comparing a register with a literal, we load the values
into two registers and compare those. Other than for int
,
the pattern is identical to what we saw with AMD64.
C | .s file | gdb |
---|---|---|
if ( ia == 2 ) { |
||
ldr r3, [fp, #-8] |
ldr r3, [r11, #-8] |
|
cmp r3, #2 |
cmp r3, #2 |
|
bne .L26 |
bne 0x10a28 |
|
if ( ca == 2 ) { |
||
ldrb r3, [fp, #-13] |
ldrb r3, [r11, #-13] |
|
cmp r3, #2 |
cmp r3, #2 |
|
bne .L30 |
bne 0x10a68 |
|
if ( uca == 2 ) { |
||
ldrb r3, [fp, #-14] |
ldrb r3, [r11, #-14] |
|
cmp r3, #2 |
cmp r3, #2 |
|
bne .L32 |
bne 0x10a88 |
|
if ( la == 2 ) { |
||
ldr r3, [fp, #-20] |
ldr r3, [r11, #-20] |
|
cmp r3, #2 |
cmp r3, #2 |
|
bne .L34 |
bne 0x10aa8 |
|
if ( 2 == ia ) { |
||
ldr r3, [fp, #-8] |
ldr r3, [r11, #-8] |
|
cmp r3, #2 |
cmp r3, #2 |
|
bne .L50 |
bne 0x10ba8 |
|
if ( 2 == ca ) { |
||
ldrb r3, [fp, #-13] |
ldrb r3, [r11, #-13] |
|
cmp r3, #2 |
cmp r3, #2 |
|
bne .L54 |
bne 0x10be8 |
|
if ( 2 == uca ) { |
||
ldrb r3, [fp, #-14] |
ldrb r3, [r11, #-14] |
|
cmp r3, #2 |
cmp r3, #2 |
|
bne .L56 |
bne 0x10c08 |
|
if ( 2 == la ) { |
||
ldr r3, [fp, #-20] |
ldr r3, [r11, #-20] |
|
cmp r3, #2 |
cmp r3, #2 |
|
bne .L58 |
bne 0x10c28 |
Comparing with a non-zero literal looks the same as comparing with zero. We still load the value into a register, and compare that register with the literal.
C | .s file | gdb |
---|---|---|
if ( 0 == ia ) { |
||
ldr r3, [fp, #-8] |
ldr r3, [r11, #-8] |
|
cmp r3, #0 |
cmp r3, #0 |
|
bne .L62 |
bne 0x10c68 |
|
if ( 0 == ca ) { |
||
ldrb r3, [fp, #-13] |
ldrb r3, [r11, #-13] |
|
cmp r3, #0 |
cmp r3, #0 |
|
bne .L66 |
bne 0x10ca8 |
|
if ( 0 == uca ) { |
||
ldrb r3, [fp, #-14] |
ldrb r3, [r11, #-14] |
|
cmp r3, #0 |
cmp r3, #0 |
|
bne .L68 |
bne 0x10cc8 |
|
if ( 0 == la ) { |
||
ldr r3, [fp, #-20] |
ldr r3, [r11, #-20] |
|
cmp r3, #0 |
cmp r3, #0 |
|
bne .L70 |
bne 0x10ce8 |
Reversing the order of the variable and literal have no effect on the generated assembly, in contrast with the equivalent assembly generated for AMD64.