Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Local variable location from DWARF info in ARM

I have a C program in file delay.c:

void delay(int num)
{
   volatile int i;
   for(i=0; i<num; i++);
}

Then I compile the program with gcc 4.6.3 on ARM emulator (armel, more specifically) with command gcc -g -O1 -o delay.o delay.c. The assembly in delay.o is:

00000000 <delay>:
   0:   e24dd008    sub sp, sp, #8
   4:   e3a03000    mov r3, #0
   8:   e58d3004    str r3, [sp, #4]
   c:   e59d3004    ldr r3, [sp, #4]
  10:   e1500003    cmp r0, r3
  14:   da000005    ble 30 <delay+0x30>
  18:   e59d3004    ldr r3, [sp, #4]
  1c:   e2833001    add r3, r3, #1
  20:   e58d3004    str r3, [sp, #4]
  24:   e59d3004    ldr r3, [sp, #4]
  28:   e1530000    cmp r3, r0
  2c:   bafffff9    blt 18 <delay+0x18>
  30:   e28dd008    add sp, sp, #8
  34:   e12fff1e    bx  lr

I want to figure out where the variable i is on the stack of function delay from debugging information. Below is the information about delay and i in .debug_info section:

<1><25>: Abbrev Number: 2 (DW_TAG_subprogram)
   <26>   DW_AT_external    : 1
   <27>   DW_AT_name        : (indirect string, offset: 0x19): delay
   <2b>   DW_AT_decl_file   : 1
   <2c>   DW_AT_decl_line   : 1
   <2d>   DW_AT_prototyped  : 1
   <2e>   DW_AT_low_pc      : 0x0
   <32>   DW_AT_high_pc     : 0x38
   <36>   DW_AT_frame_base  : 0x0      (location list)
   <3a>   DW_AT_sibling     : <0x59>
...
<2><4b>: Abbrev Number: 4 (DW_TAG_variable)
   <4c>   DW_AT_name        : i
   <4e>   DW_AT_decl_file   : 1
   <4f>   DW_AT_decl_line   : 3
   <50>   DW_AT_type        : <0x60>
   <54>   DW_AT_location    : 0x20     (location list)

It shows that the location of i is in the location list. So I output the location list:

Offset   Begin    End      Expression
00000000 00000000 00000004 (DW_OP_breg13 (r13): 0)
00000000 00000004 00000038 (DW_OP_breg13 (r13): 8)
00000000 <End of list>
00000020 0000000c 00000020 (DW_OP_fbreg: -12)
00000020 00000024 00000028 (DW_OP_reg3 (r3))
00000020 00000028 00000038 (DW_OP_fbreg: -12)
00000020 <End of list>

From address 4 to 38, the frame base of delay should be r13 + 8. So from address c to 20 and from address 28 to 38, the location of i is r13 + 8 -12 = r13 - 4.

However, from the assembly, we can know that there is no location r13 - 4 and i is apparently at location r13 + 4.

Do I miss some calculation step? Anyone can explain the difference of i's location between calculation from debugging information and in assembly?

Thanks in advance!

like image 421
LostBenjamin Avatar asked Nov 17 '17 21:11

LostBenjamin


2 Answers

TL;DR The analysis in the question is correct and the discrepancy is a bug in one of the gcc components (GNU Arm Embedded Toolchain is an obvious place to log one).

As it stands, this other answer is incorrect because it erroneously conflates the value of the stack pointer on evaluation of a location expression with the earlier value of the stack pointer on entry to the function.

As far as the DWARF is concerned, the location of i varies with the program counter. Consider, for example, the text address delay+0x18. At this point, the location of i is given by DW_OP_fbreg(-12), i.e. 12 bytes below the frame base. The frame base is given by the parent DW_TAG_subprogram's DW_AT_frame_base attribute which, in this case, is also dependent on the program counter: for delay+0x18 its expression is DW_OP_breg13(8), i.e. r13 + 8. Importantly, this calculation uses the current value of r13, i.e. the value of r13 when the program counter is equal to delay+0x18.

Thus the DWARF asserts that, at delay+0x18, i is located at r13 + 8 - 12, i.e. 4 bytes below the bottom of the existing stack. Inspection of the assembly shows that, at delay+018, i should be found 4 bytes above the bottom of the stack. Therefore the DWARF is in error and whatever generated it is defective.

One can demonstrate the bug using gdb with a simple wrapper around the test case provided in the question:

$ cat delay.c
void delay(int num)
{
   volatile int i;
   for(i=0; i<num; i++);
}
$ gcc-4.6 -g -O1 -c delay.c
$ cat main.c
void delay(int);

int main(int argc, char **argv) {
    delay(3);
}
$ gcc-4.6 -o test main.c delay.o
$ gdb ./test
.
.
.
(gdb) 

Set a breakpoint at delay+0x18 and run to the second occurrence (where we expect i to be 1):

(gdb) break *delay+0x18
Breakpoint 1 at 0x103cc: file delay.c, line 4.
(gdb) run
Starting program: /home/pi/test 

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb) cont
Continuing.

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb)

We know from the disassembly that i is four bytes above the stack pointer. Indeed, there it is:

(gdb) print *((int *)($r13 + 4))
$1 = 1
(gdb)

However, the bogus DWARF means that gdb looks in the wrong place:

(gdb) print i
$2 = 0
(gdb)

As explained above, the DWARF is incorrectly giving the location of i at four bytes below the stack pointer. There's a zero there, hence the reported value of i:

(gdb)  print *((int *)($r13 - 4))
$3 = 0
(gdb)

This isn't a coincidence. A magic number written into this bogus location below the stack pointer reappears when gdb is asked to print i:

(gdb) set *((int *)($r13 - 4)) = 42
(gdb) print i
$6 = 42
(gdb)

Thus, at delay+0x18, the DWARF incorrectly encodes the location of i as r13 - 4 even though its true location is r13 + 4.

One can go a step further by editing the compilation unit by hand and replacing DW_OP_fbreg(-12) (bytes 0x91 0x74) with DW_OP_fbreg(-4) (bytes 0x91 0x7c). This gives

$ readelf --debug-dump=loc delay.modified.o 
Contents of the .debug_loc section:

Offset   Begin            End              Expression
00000000 00000000 00000004 (DW_OP_breg13 (r13): 0)
0000000c 00000004 00000038 (DW_OP_breg13 (r13): 8)
00000018 <End of list>
00000020 0000000c 00000020 (DW_OP_fbreg: -4)
0000002c 00000024 00000028 (DW_OP_reg3 (r3))
00000037 00000028 00000038 (DW_OP_fbreg: -4)
00000043 <End of list>

$

In other words, the DWARF has been corrected so that at, e.g., delay+0x18 the location of i is given as frame base - 4 = r13 + 8 - 4 = r13 + 4, matching the assembly. Repeating the gdb experiment with the corrected DWARF shows the expected value of i each time around the loop:

$ gcc-4.6 -o test.modified main.c delay.modified.o
$ gdb ./test.modified 
.
.
.
(gdb) break *delay+0x18
Breakpoint 1 at 0x103cc: file delay.c, line 4.
(gdb) run
Starting program: /home/pi/test.modified 

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb) print i
$1 = 0
(gdb) cont
Continuing.

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb) print i
$2 = 1
(gdb) cont
Continuing.

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb) print i
$3 = 2
(gdb) cont
Continuing.
[Inferior 1 (process 30954) exited with code 03]
(gdb) 
like image 200
Robert Harris Avatar answered Dec 31 '22 08:12

Robert Harris


I am not agree with the OP's asm analysis:

00000000 <delay>:                         ; so far, let's suppose sp = sp(0) 
0:   e24dd008    sub sp, sp, #8           ; sp = sp(0) - 8
4:   e3a03000    mov r3, #0               ; r3 = 0 
8:   e58d3004    str r3, [sp, #4]         ; store the value of r3 in (sp + 4)
c:   e59d3004    ldr r3, [sp, #4]         ; load (sp + 4) in r3
10:   e1500003    cmp r0, r3              ; compare r3 and r0  
14:   da000005    ble 30 <delay+0x30>     ; go to end of loop
18:   e59d3004    ldr r3, [sp, #4]        ; i is in r3, and it is being loaded from
                                          ; (sp + 4), that is, 
                                          ; sp(i) = sp(0) - 8 + 4 = sp(0) - 4
1c:   e2833001    add r3, r3, #1          ; r3 = r3 + 1, that is, increment i
20:   e58d3004    str r3, [sp, #4]        ; store i (which is in r3) in (sp + 4),
                                          ; being again sp(i) = sp(0) - 8 + 4 = \
                                          ; sp(0) - 4
24:   e59d3004    ldr r3, [sp, #4]        ; load sp + 4 in r3
28:   e1530000    cmp r3, r0              ; compare r3 and r0
2c:   bafffff9    blt 18 <delay+0x18>     ; go to init of loop
30:   e28dd008    add sp, sp, #8          ; sp = sp + 8
34:   e12fff1e    bx  lr                  ; 

So i is located in sp(0) - 4, which matchs with the dwarf analysis (which says that i is being located in 0 + 8 - 12)

Edit in order to add information regarding my DWARF analysis:

According to this line: 00000020 0000000c 00000020 (DW_OP_fbreg: -12) , being DW_OP_fbreg :

The DW_OP_fbreg operation provides a signed LEB128 offset from the address specified by the location description in the DW_AT_frame_base attribute of the current function. (This is typically a “stack pointer” register plus or minus some offset. On more sophisticated systems it might be a location list that adjusts the offset according to changes in the stack pointer as the PC changes.)

,the address is frame_base + offset, where:

  • frame_base : is the stack pointer +/- some offset, and according to the previous line (00000000 00000004 00000038 (DW_OP_breg13 (r13): 8)), from 00000004 to 00000038, it has an offset of +8 (r13 is SP)
  • offset: obviously it is -12

Given that, DWARF indicates that it is pointing to sp(0) + 8 - 12 = sp(0) - 4

like image 20
Jose Avatar answered Dec 31 '22 09:12

Jose