Matt Galloway

My home on the 'net.

Assembly - beware local label names with "-dead_strip" option!

I came across a very strange bug whilst developing an iOS application whereby the application would seg fault and whilst stepping through the code I found it was going all over the place. This lead me to run the application through otool, and I discovered that half the code for a function was missing!

Here is an example of what happened…

Consider the following C file. It’s just a very simple function that has some inline assembly (to count down from 10 to 0) and a very simple function that does absolutely nothing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void func() {
    int tmp;
    __asm__ __volatile__ (
        "\tmov %0, #10\n"

        ".loop:\n"
        "\tsubs %0, %0, #1\n"
        "\tbne .loop\n"

        : "=r" (tmp)
        : "r" (tmp)
    );
}

void funcB() {
}

Let’s see what happens when we compile it for iOS…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    .section __TEXT,__text,regular
    .section __TEXT,__textcoal_nt,coalesced
    .section __TEXT,__const_coal,coalesced
    .section __TEXT,__picsymbolstub4,symbol_stubs,none,16
    .text
    .align 2
    .globl _func
_func:
    @ args = 0, pretend = 0, frame = 4
    @ frame_needed = 1, uses_anonymous_args = 0
    stmfd   sp!, {r7, lr}
    add     r7, sp, #0
    sub     sp, sp, #4
    ldr     r3, [sp]
            mov r3, #10
.loop:
    subs r3, r3, #1
    bne .loop

    str     r3, [sp]
    sub     sp, r7, #0
    ldmfd   sp!, {r7, pc}
    .align 2
    .globl _funcB
_funcB:
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 1, uses_anonymous_args = 0
    stmfd   sp!, {r7, lr}
    add     r7, sp, #0
    ldmfd   sp!, {r7, pc}
    .subsections_via_symbols

That all looks fairly normal and we could quite happily believe that was going to work just fine. So, let’s use this function in a test application. The code below is just a very simple application that calls the ‘func()’ function and then returns.

1
2
3
4
5
void func();
int main() {
    func();
    return 0;
}

So now let’s see what happens when we link this with the same options that you would have on by default in an iOS application (hint: -dead_strip is active).

Output from otool -vV -t on the linked application:

1
2
3
4
5
6
7
8
9
10
11
12
13
_main:
00002fa0        e92d4080        push    {r7, lr}
00002fa4        e28d7000        add     r7, sp, #0      @ 0x0
00002fa8        eb000002        bl      _func
00002fac        e3a03000        mov     r3, #0  @ 0x0
00002fb0        e1a00003        mov     r0, r3
00002fb4        e8bd8080        pop     {r7, pc}
_func:
00002fb8        e92d4080        push    {r7, lr}
00002fbc        e28d7000        add     r7, sp, #0      @ 0x0
00002fc0        e24dd004        sub     sp, sp, #4      @ 0x4
00002fc4        e59d3000        ldr     r3, [sp]
00002fc8        e3a0300a        mov     r3, #10 @ 0xa

What?! Where’s the rest of the func() code?! Not only is it missing, but it would appear that func() just simply stops without ever returning?! That looks very suspicious… So, let’s compile it without the -dead_strip option:

Output from ‘otool -vV -t’ on the linked application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_main:
00002f80        e92d4080        push    {r7, lr}
00002f84        e28d7000        add     r7, sp, #0      @ 0x0
00002f88        eb000002        bl      _func
00002f8c        e3a03000        mov     r3, #0  @ 0x0
00002f90        e1a00003        mov     r0, r3
00002f94        e8bd8080        pop     {r7, pc}
_func:
00002f98        e92d4080        push    {r7, lr}
00002f9c        e28d7000        add     r7, sp, #0      @ 0x0
00002fa0        e24dd004        sub     sp, sp, #4      @ 0x4
00002fa4        e59d3000        ldr     r3, [sp]
00002fa8        e3a0300a        mov     r3, #10 @ 0xa
.loop:
00002fac        e2533001        subs    r3, r3, #1      @ 0x1
00002fb0        1afffffd        bne     .loop
00002fb4        e58d3000        str     r3, [sp]
00002fb8        e247d000        sub     sp, r7, #0      @ 0x0
00002fbc        e8bd8080        pop     {r7, pc}
_funcB:
00002fc0        e92d4080        push    {r7, lr}
00002fc4        e28d7000        add     r7, sp, #0      @ 0x0
00002fc8        e8bd8080        pop     {r7, pc}

Ah, that’s better! The loop is back and so is the return. Also, funcB() is still in there. So, what has happened you may ask. Well, -dead_strip is designed to remove symbols from a binary that are not required. So we’d expect funcB to be removed, but not .loop as it’s part of func. However, if we look closer, what has happened is that .loop has become a top level symbol rather than a symbol local to the func symbol. So -dead_strip assumed that it was a symbol not used anywhere (as it’s only accessed from within .loop itself) and so it removed it, resulting in a completely mangled application binary.

To stop this happening you must always prefix your local symbols with ‘L’ (as per the GCC documentation!). But I think this is an excellent example of what can go wrong if you just do 1 tiny thing wrong with inline assembly.

As a side note, I decided to try compiling/assembling/linking all of the above for Android as well. Interestingly the results were different. First I’ll show the outputs of the various stages and then explain the results.

The assembly generated for Android:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    .arch armv6
    .fpu softvfp
    .eabi_attribute 20, 1
    .eabi_attribute 21, 1
    .eabi_attribute 23, 3
    .eabi_attribute 24, 1
    .eabi_attribute 25, 1
    .eabi_attribute 26, 2
    .eabi_attribute 30, 6
    .eabi_attribute 18, 4
    .file   "test.c"
    .text
    .align  2
    .global func
    .type   func, %function
func:
    @ args = 0, pretend = 0, frame = 8
    @ frame_needed = 1, uses_anonymous_args = 0
    @ link register save eliminated.
    str     fp, [sp, #-4]!
    add     fp, sp, #0
    sub     sp, sp, #12
    ldr     r3, [fp, #-8]
#APP
@ 3 "test.c" 1
            mov r3, #10
.loop:
    subs r3, r3, #1
    bne .loop

@ 0 "" 2
    str     r3, [fp, #-8]
    add     sp, fp, #0
    ldmfd   sp!, {fp}
    bx      lr
    .size   func, .-func
    .align  2
    .global funcB
    .type   funcB, %function
funcB:
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 1, uses_anonymous_args = 0
    @ link register save eliminated.
    str     fp, [sp, #-4]!
    add     fp, sp, #0
    add     sp, fp, #0
    ldmfd   sp!, {fp}
    bx      lr
    .size   funcB, .-funcB
    .ident  "GCC: (GNU) 4.4.3"
    .section        .note.GNU-stack,"",%progbits

Disassembly of linked app for Android (without stripping):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
000082e0 <main>:
    82e0:       e92d4800        push    {fp, lr}
    82e4:       e28db004        add     fp, sp, #4      ; 0x4
    82e8:       eb000002        bl      82f8 <func>
    82ec:       e3a03000        mov     r3, #0  ; 0x0
    82f0:       e1a00003        mov     r0, r3
    82f4:       e8bd8800        pop     {fp, pc}

000082f8 <func>:
    82f8:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)
    82fc:       e28db000        add     fp, sp, #0      ; 0x0
    8300:       e24dd00c        sub     sp, sp, #12     ; 0xc
    8304:       e51b3008        ldr     r3, [fp, #-8]
    8308:       e3a0300a        mov     r3, #10 ; 0xa

0000830c <.loop>:
    830c:       e2533001        subs    r3, r3, #1      ; 0x1
    8310:       1afffffd        bne     830c <.loop>
    8314:       e50b3008        str     r3, [fp, #-8]
    8318:       e28bd000        add     sp, fp, #0      ; 0x0
    831c:       e8bd0800        pop     {fp}
    8320:       e12fff1e        bx      lr

00008324 <funcB>:
    8324:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)
    8328:       e28db000        add     fp, sp, #0      ; 0x0
    832c:       e28bd000        add     sp, fp, #0      ; 0x0
    8330:       e8bd0800        pop     {fp}
    8334:       e12fff1e        bx      lr

Disassembly of linked app for Android (after using strip command):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
000082e0 <main>:
    82e0:       e92d4800        push    {fp, lr}
    82e4:       e28db004        add     fp, sp, #4      ; 0x4
    82e8:       eb000002        bl      82f8 <func>
    82ec:       e3a03000        mov     r3, #0  ; 0x0
    82f0:       e1a00003        mov     r0, r3
    82f4:       e8bd8800        pop     {fp, pc}

000082f8 <func>:
    82f8:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)
    82fc:       e28db000        add     fp, sp, #0      ; 0x0
    8300:       e24dd00c        sub     sp, sp, #12     ; 0xc
    8304:       e51b3008        ldr     r3, [fp, #-8]
    8308:       e3a0300a        mov     r3, #10 ; 0xa
    830c:       e2533001        subs    r3, r3, #1      ; 0x1
    8310:       1afffffd        bne     830c <func+0x14>
    8314:       e50b3008        str     r3, [fp, #-8]
    8318:       e28bd000        add     sp, fp, #0      ; 0x0
    831c:       e8bd0800        pop     {fp}
    8320:       e12fff1e        bx      lr

You can see that the Android GCC has done a much better job at stripping out the symbols. This is because of a subtle .size attribute given to functions in the assembly for Linux (and therefore Android) which tells the assembler how big the function is. However, this doesn’t exist on Mac and the size is calculated by taking the distance between the start of a symbol and the next symbol.

This actually helps us to understand a bit more what actually went wrong. If you look back up at the assembly generated for iOS and Android you’ll see that the .loop appears as a top level symbol, which is confirmed by running ‘nm’ on the resulting object file. This sounds all wrong, since the .loop symbol should really be local to the func() function. The reason being because you need to prefix local symbols with ‘L’. If we change .loop for Lloop in the sample file, then these are the resulting outputs showing the iOS linker doing the right thing this time even with -dead_strip enabled.

Compiled output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    .section __TEXT,__text,regular
    .section __TEXT,__textcoal_nt,coalesced
    .section __TEXT,__const_coal,coalesced
    .section __TEXT,__picsymbolstub4,symbol_stubs,none,16
    .text
    .align 2
    .globl _func
_func:
    @ args = 0, pretend = 0, frame = 4
    @ frame_needed = 1, uses_anonymous_args = 0
    stmfd   sp!, {r7, lr}
    add     r7, sp, #0
    sub     sp, sp, #4
    ldr     r3, [sp]
            mov r3, #10
Lloop:
    subs r3, r3, #1
    bne Lloop

    str     r3, [sp]
    sub     sp, r7, #0
    ldmfd   sp!, {r7, pc}
    .align 2
    .globl _funcB
_funcB:
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 1, uses_anonymous_args = 0
    stmfd   sp!, {r7, lr}
    add     r7, sp, #0
    ldmfd   sp!, {r7, pc}
    .subsections_via_symbols

Disassembly of linked application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_main:
00002f8c        e92d4080        push    {r7, lr}
00002f90        e28d7000        add     r7, sp, #0      @ 0x0
00002f94        eb000002        bl      _func
00002f98        e3a03000        mov     r3, #0  @ 0x0
00002f9c        e1a00003        mov     r0, r3
00002fa0        e8bd8080        pop     {r7, pc}
_func:
00002fa4        e92d4080        push    {r7, lr}
00002fa8        e28d7000        add     r7, sp, #0      @ 0x0
00002fac        e24dd004        sub     sp, sp, #4      @ 0x4
00002fb0        e59d3000        ldr     r3, [sp]
00002fb4        e3a0300a        mov     r3, #10 @ 0xa
00002fb8        e2533001        subs    r3, r3, #1      @ 0x1
00002fbc        1afffffd        bne     0x2fb8
00002fc0        e58d3000        str     r3, [sp]
00002fc4        e247d000        sub     sp, r7, #0      @ 0x0
00002fc8        e8bd8080        pop     {r7, pc}

This all serves to illustrate the point that it’s worth knowing about the options of your compiler, assembler & linker and understanding how everything fits together.

Comments