Matt Galloway

My home on the 'net.

[UPDATED - Not a bug!] Another interesting compiler bug (in Apple's LLVM this time)

I came across another interesting compiler bug today. I’m not going to go into too much detail about it, but the problem code is this:

1
2
3
4
5
6
7
8
9
10
void a() {
    int a = 0;
    int b = 0;
    __asm__("\n"
            "\tmov %0, 0\n"
            "\tldr %0, %1\n"
            "\tmov %1, 0\n"
            : "=r"(a), "+m"(b)
           );
}

To break that down a bit, the function is doing this:

  • Initialise a couple of variables and set them to 0.
1
2
int a = 0;
int b = 0;
  • Then perform some inline assembly using the variables. This just puts 0 into the register that will hold our ‘a’ variable, then loads the value of ‘b’ into ‘a’, then sets ‘b’ to 0.
1
2
3
4
5
6
__asm__("\n"
        "\tmov %0, 0\n"
        "\tldr %0, %1\n"
        "\tmov %1, 0\n"
        : "=r"(a), "+m"(b)
       );

This is of course a contrived example, but it illustrates the bug. The output assembly from LLVM-GCC or clang is (for ARM architecture):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    .globl  _a
    .align  2
    .code   16
    .thumb_func     _a
_a:
    sub     sp, #8
    movs    r0, #0
    movt    r0, #0
    str     r0, [sp, #4]
    str     r0, [sp]
    mov     r0, sp
    @ InlineAsm Start

    mov r0, 0
    ldr r0, [r0]
    mov [r0], 0

    @ InlineAsm End
    str     r0, [sp, #4]
    add     sp, #8
    bx      lr

The interesting bit is the inline assembly. You’ll notice that it’s doing something very stupid. It’s choosing the same register for both operands (it’s choosing r0). This is completely wrong, and will lead to a runtime crash in this case due to the dereference of 0.

I did a bit of hunting and it appears to be a problem in generating the LLVM bytecode as the problem manifests itself before the LLVM bytecode is compiled down into instructions, like so:

1
2
3
4
5
6
7
8
9
define void @a() nounwind ssp {
  %a = alloca i32, align 4
  %b = alloca i32, align 4
  store i32 0, i32* %a, align 4
  store i32 0, i32* %b, align 4
  %1 = call i32 asm "\0A\09mov $0, 0\0A\09ldr $0, $1\0A\09mov $1, 0\0A", "=r,=*m,*m"(i32* %b, i32* %b) nounwind, !srcloc !0
  store i32 %1, i32* %a, align 4
  ret void
}

You can see here that the ‘%a’ (i.e. variable ‘a’) is never referenced, only ‘%b’ (i.e. variable ‘a’). This is not what we’d expect at all given we’re referencing both variables in the code.

I found this quite interesting :–).

Update: Not a bug!

I’ve actually found out that this isn’t a bug! That’s good news, right? It’s quite a subtle thing, but the heart of the problem can be explained after understanding the modifiers to operands on inline assembly. The problem is that we’re not specifying that ‘a’ is clobbered early. In the assembly, we’re writing to it before reading we’ve finished using all input operands (only ‘b’ in this case) so we’re meant to mark it like that. It’s just luck that GCC does the right thing – Apple’s LLVM is doing the right thing and using less registers!

So the correct code is this:

1
2
3
4
5
6
7
8
9
10
void a() {
    int a = 0;
    int b = 0;
    __asm__("\n"
            "\tmov %0, 0\n"
            "\tldr %0, %1\n"
            "\tmov %1, 0\n"
            : "=&r"(a), "+m"(b)
           );
}

Which results in the following assembly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    .globl  _a
    .align  2
    .code   16
    .thumb_func     _a
_a:
    sub     sp, #8
    movs    r0, #0
    movt    r0, #0
    str     r0, [sp, #4]
    str     r0, [sp]
    mov     r0, sp
    @ InlineAsm Start

    mov r1, 0
    ldr r1, [r0]
    mov [r0], 0

    @ InlineAsm End
    str     r1, [sp, #4]
    add     sp, #8
    bx      lr

And the following LLVM:

1
2
3
4
5
6
7
8
9
define void @a() nounwind ssp {
  %a = alloca i32, align 4
  %b = alloca i32, align 4
  store i32 0, i32* %a, align 4
  store i32 0, i32* %b, align 4
  %1 = call i32 asm "\0A\09mov $0, 0\0A\09ldr $0, $1\0A\09mov $1, 0\0A", "=&r,=*m,*m"(i32* %b, i32* %b) nounwind, !srcloc !0
  store i32 %1, i32* %a, align 4
  ret void
}

Comments