Science and technology

Compiler optimization and its impact on debugger line info

In my previous article, I described the DWARF info used to map common and inlined capabilities between an executable binary and its supply code. Functions may be dozens of traces, so that you would possibly prefer to know particularly the place the processor is in your supply code. The compiler contains info mapping between directions and particular traces within the supply code to offer a exact location. In this text, I describe line mapping info, and a few of the points attributable to compiler optimizations.

Start with the identical instance code from the earlier article:

#embrace <stdlib.h>
#embrace <stdio.h>

int a;
double b;

int
principal(int argc, char* argv[])
{
	a = atoi(argv[1]);
	b = atof(argv[2]);
	a = a + 1;
	b = b / 42.0;
	printf ("a = %d, b = %fn", a, b);
	return 0;
}

The compiler solely contains the road mapping info when the code is compiled with debugging info enabled (the -g possibility):

$ gcc -O2 -g instance.c -o instance

Examining line quantity info

Line info is saved in a machine readable format, however human readable output may be generated with llvm-objdump or odjdump.

$ llvm-objdump --line-numbers instance

For the principle operate, you get output itemizing the meeting code instruction with the file and line quantity related to the instruction:

0000000000401060 <principal>:
; principal():
; /house/wcohen/current/202207youarehere/instance.c:9
  401060: 53                      		 pushq    %rbx
; /usr/embrace/stdlib.h:364
  401061: 48 8b 7e 08             		 movq    8(%rsi), %rdi
; /house/wcohen/current/202207youarehere/instance.c:9
  401065: 48 89 f3                		 movq    %rsi, %rbx
; /usr/embrace/stdlib.h:364
  401068: ba 0a 00 00 00          		 movl    $10, %edx
  40106d: 31 f6                   		 xorl    %esi, %esi
  40106f: e8 dc ff ff ff          		 callq    0x401050 <strtol@plt>
; /usr/embrace/bits/stdlib-float.h:27
  401074: 48 8b 7b 10             		 movq    16(%rbx), %rdi
  401078: 31 f6                   		 xorl    %esi, %esi
; /usr/embrace/stdlib.h:364
  40107a: 89 05 c8 2f 00 00       		 movl    %eax, 12232(%rip)   	# 0x404048 <a>
; /usr/embrace/bits/stdlib-float.h:27
  401080: e8 ab ff ff ff          		 callq    0x401030 <strtod@plt>
; /house/wcohen/current/202207youarehere/instance.c:12
  401085: 8b 05 bd 2f 00 00       		 movl    12221(%rip), %eax   	# 0x404048 <a>
; /house/wcohen/current/202207youarehere/instance.c:14
  40108b: bf 10 20 40 00          		 movl    $4202512, %edi      	# imm = 0x402010
; /house/wcohen/current/202207youarehere/instance.c:13
  401090: f2 0f 5e 05 88 0f 00 00 		 divsd    3976(%rip), %xmm0   	# 0x402020 <__dso_handle+0x18>
  401098: f2 0f 11 05 a0 2f 00 00 		 movsd    %xmm0, 12192(%rip)  	# 0x404040 <b>
; /house/wcohen/current/202207youarehere/instance.c:12
  4010a0: 8d 70 01                		 leal    1(%rax), %esi
; /house/wcohen/current/202207youarehere/instance.c:14
  4010a3: b8 01 00 00 00          		 movl    $1, %eax
; /house/wcohen/current/202207youarehere/instance.c:12
  4010a8: 89 35 9a 2f 00 00       		 movl    %esi, 12186(%rip)   	# 0x404048 <a>
; /house/wcohen/current/202207youarehere/instance.c:14
  4010ae: e8 8d ff ff ff          		 callq    0x401040 <printf@plt>
; /house/wcohen/current/202207youarehere/instance.c:16
  4010b3: 31 c0                   		 xorl    %eax, %eax
  4010b5: 5b                      		 popq    %rbx
  4010b6: c3

The first instruction at 0x401060 maps to the unique supply code file instance.c line 9, the opening { for the principle operate.

The subsequent instruction 0x401061 maps to line 364 of stdlib.h line 364, the inlined atoi operate. This is organising one of many arguments to the later strtol name.

The instruction 0x401065 can also be related to the opening { of the principle operate.

Instructions 0x401068 and 0x40106d set the remaining arguments for the strtol name that takes place at 0x40106f. In this case, you possibly can see that the compiler has reordered the directions and causes some bouncing between line 9 of instance.c and line 364, or the stdlib.h embrace file, as you step by the directions on the debugger.

You may see some mixing of directions for traces 12, 13, and 14 from instance.c within the output of llvm-objdump above. The compiler has moved the divide directions (0x40190) for line 13 earlier than a few of the directions for line 12 to cover the latency of the divide. As you step by the directions within the debugger for this code, you see the debugger soar backwards and forwards between traces reasonably than doing all of the directions from one line earlier than shifting on to the following line. Also discover as you step although that line 13 with the divide operation was not proven, however the divide positively occurred to supply the output. You can see GDB bouncing between traces when stepping by the  program’s principal operate:

(gdb) run 1 2
Starting program: /house/wcohen/current/202207youarehere/instance 1 2
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, principal (argc=3, argv=0x7fffffffdbe8) at /usr/embrace/stdlib.h:364
364      return (int) strtol (__nptr, (char **) NULL, 10);
(gdb) print $computer
$10 = (void (*)()) 0x401060 <principal>
(gdb) subsequent
10   	 a = atoi(argv[1]);
(gdb) print $computer
$11 = (void (*)()) 0x401061 <principal+1>
(gdb) subsequent
11   	 b = atof(argv[2]);
(gdb) print $computer
$12 = (void (*)()) 0x401074 <principal+20>
(gdb) subsequent
10   	 a = atoi(argv[1]);
(gdb) print $computer
$13 = (void (*)()) 0x40107a <principal+26>
(gdb) subsequent
11   	 b = atof(argv[2]);
(gdb) print $computer
$14 = (void (*)()) 0x401080 <principal+32>
(gdb) subsequent
12   	 a = a + 1;
(gdb) print $computer
$15 = (void (*)()) 0x401085 <principal+37>
(gdb) subsequent
14   	 printf ("a = %d, b = %fn", a, b);
(gdb) print $computer
$16 = (void (*)()) 0x4010ae <principal+78>
(gdb) subsequent
a = 2, b = 0.047619
15   	 return 0;
(gdb) print $computer
$17 = (void (*)()) 0x4010b3 <principal+83>

With this easy instance, you possibly can see that the order of directions doesn’t match the unique supply code. When this system is operating usually, you’d by no means observe these adjustments. However, they’re fairly seen when utilizing a debugger to step by the code. The boundaries between traces of code change into blurred. This has different implications. When you resolve to set a breakpoint to a line following a line with variable replace, the compiler scheduler might have moved the variable after the situation you count on the variable to be up to date, and also you don’t get the anticipated worth for the variable on the breakpoint.

Which of the directions for a line get the breakpoint?

With the earlier instance.c, the compiler generated a number of directions to implement particular person traces of code. How does the debugger know which of these directions needs to be the one which it locations the breakpoint on? There’s a further assertion flag within the line info that marks the advisable places to position the breakpoints. You can see these directions marked with S within the column beneath SBPE in eu-readelf --debug-dump=decodedline instance:

DWARF part [31] '.debug_line' at offset 0x50fd:

 CU [c] instance.c
  line:col SBPE* disc isa op tackle (Statement Block Prologue Epilogue *End)
  /house/wcohen/current/202207youarehere/instance.c (mtime: 0, size: 0)
     9:1   S        0   0  0 0x0000000000401060 <principal>
    10:2   S        0   0  0 0x0000000000401060 <principal>
  /usr/embrace/stdlib.h (mtime: 0, size: 0)
   362:1   S        0   0  0 0x0000000000401060 <principal>
   364:3   S        0   0  0 0x0000000000401060 <principal>
  /house/wcohen/current/202207youarehere/instance.c (mtime: 0, size: 0)
     9:1            0   0  0 0x0000000000401060 <principal>
  /usr/embrace/stdlib.h (mtime: 0, size: 0)
   364:16           0   0  0 0x0000000000401061 <principal+0x1>
   364:16           0   0  0 0x0000000000401065 <principal+0x5>
  /house/wcohen/current/202207youarehere/instance.c (mtime: 0, size: 0)
     9:1            0   0  0 0x0000000000401065 <principal+0x5>
  /usr/embrace/stdlib.h (mtime: 0, size: 0)
   364:16           0   0  0 0x0000000000401068 <principal+0x8>
   364:16           0   0  0 0x000000000040106f <principal+0xf>
   364:16           0   0  0 0x0000000000401074 <principal+0x14>
  /usr/embrace/bits/stdlib-float.h (mtime: 0, size: 0)
    27:10           0   0  0 0x0000000000401074 <principal+0x14>
  /usr/embrace/stdlib.h (mtime: 0, size: 0)
   364:10           0   0  0 0x000000000040107a <principal+0x1a>
  /house/wcohen/current/202207youarehere/instance.c (mtime: 0, size: 0)
    11:2   S        0   0  0 0x0000000000401080 <principal+0x20>
  /usr/embrace/bits/stdlib-float.h (mtime: 0, size: 0)
    25:1   S        0   0  0 0x0000000000401080 <principal+0x20>
    27:3   S        0   0  0 0x0000000000401080 <principal+0x20>
    27:10           0   0  0 0x0000000000401080 <principal+0x20>
    27:10           0   0  0 0x0000000000401085 <principal+0x25>
  /house/wcohen/current/202207youarehere/instance.c (mtime: 0, size: 0)
    12:2   S        0   0  0 0x0000000000401085 <principal+0x25>
    12:8            0   0  0 0x0000000000401085 <principal+0x25>
    14:2            0   0  0 0x000000000040108b <principal+0x2b>
    13:8            0   0  0 0x0000000000401090 <principal+0x30>
    13:4            0   0  0 0x0000000000401098 <principal+0x38>
    12:8            0   0  0 0x00000000004010a0 <principal+0x40>
    14:2            0   0  0 0x00000000004010a3 <principal+0x43>
    12:4            0   0  0 0x00000000004010a8 <principal+0x48>
    13:2   S        0   0  0 0x00000000004010ae <principal+0x4e>
    14:2   S        0   0  0 0x00000000004010ae <principal+0x4e>
    15:2   S        0   0  0 0x00000000004010b3 <principal+0x53>
    16:1            0   0  0 0x00000000004010b3 <principal+0x53>
    16:1            0   0  0 0x00000000004010b6 <principal+0x56>
    16:1       *    0   0  0 0x00000000004010b6 <principal+0x56>

  • Groups of directions are delimited by the trail to the supply file for these directions.
  • The left column comprises the road quantity and column that the instruction maps again to, adopted by the flags.
  • The hexadecimal quantity is the tackle of the instruction, adopted by the offset into the operate of the instruction.

If you look fastidiously on the output, you see that some directions map again to a number of traces within the code. For instance, 0x0000000000401060 maps to each line 9 and 10 of instance.c. The identical instruction additionally maps to traces 362 and 364 of /usr/embrace/stdlib.h. The mappings are usually not one-to-one. One line of supply code might map to a number of directions, and one instruction might map to a number of traces of code. When the debugger decides to print out a single line mapping for an instruction, it won’t be the one that you simply count on.

Merging and eliminating of traces

As you noticed within the output of the detailed line mapping info, mappings are usually not one-to-one. There are circumstances the place the compiler can get rid of directions as a result of they don’t have any impact on the ultimate results of this system. The compiler may additionally merge directions from separate traces by optimizations, akin to frequent subexpression elimination (CSE), and omit that the instruction might have come from multiple place within the code.

The following instance was compiled on an x86_64 Fedora 36 machine, utilizing GCC-12.2.1. Depending on the actual atmosphere, chances are you’ll not get the identical outcomes, as a result of totally different variations of compilers might optimize the code otherwise.

Note the if-else assertion within the code. Both have statements doing the identical costly divides. The compiler elements out the divide operation.

#embrace <stdlib.h>
#embrace <stdio.h>

int
principal(int argc, char* argv[])
{
    int a,b,c;
    a = atoi(argv[1]);
    b = atoi(argv[2]);
    if (b) {
   	 c = 100/a;
    } else {
   	 c = 100/a;
    }
    printf ("a = %d, b = %d, c = %dn", a, b, c);
    return 0;
}

Looking at objdump -dl whichline, you see one divide operation within the binary:

/house/wcohen/current/202207youarehere/whichline.c:13
  401085:    b8 64 00 00 00  		 mov	$0x64,%eax
  40108a:    f7 fb           		 idiv   %ebx

Line 13 is among the traces with a divide, however you would possibly suspect that there are different line numbers related to these addresses. Look on the output of eu-readelf --debug-dump=decodedline whichline to see whether or not there are different line numbers related to these addresses.

Line 11, the place the opposite divide happens, isn’t on this listing:

  /usr/embrace/stdlib.h (mtime: 0, size: 0)
   364:16       0   0  0 0x0000000000401082 <principal+0x32>
   364:16       0   0  0 0x0000000000401085 <principal+0x35>
  /house/wcohen/current/202207youarehere/whichline.c (mtime: 0, size: 0)
   10:2   S    	0   0  0 0x0000000000401085 <principal+0x35>
   13:3   S    	0   0  0 0x0000000000401085 <principal+0x35>
   15:2   S    	0   0  0 0x0000000000401085 <principal+0x35>
   13:5        	0   0  0 0x0000000000401085 <principal+0x35>

If the outcomes are unused, the compiler might utterly get rid of producing code for some traces.

Consider the next instance, the place the else clause computes c = 100 * a, however doesn’t use it:

#embrace <stdlib.h>
#embrace <stdio.h>

int
principal(int argc, char* argv[])
{
    int a,b,c;
    a = atoi(argv[1]);
    b = atoi(argv[2]);
    if (b) {
   	 c = 100/a;
   	 printf ("a = %d, b = %d, c = %dn", a, b, c);
    } else {
   	 c = 100 * a;
   	 printf ("a = %d, b = %dn", a, b);
    }
    return 0;
}

Programming and improvement

Compile get rid of.c with GCC:

$ gcc -O2 -g get rid of.c -o get rid of

When trying by the output generated by objdump -dl get rid of, there’s no signal of the multiplication for 100 * a (line 14) of get rid of.c. The compiler has decided that the worth was not used and eradicated it.

When trying by the output of objdump -dl get rid of, there is no such thing as a:

/house/wcohen/current/202207youarehere/get rid of.c:14

Maybe it’s hidden as one of many different views of line info. You can use eu-readelf with the --debug-dump choice to get an entire view of the road info:

$ eu-readelf --debug-dump=decodedline get rid of > get rid of.traces

It seems that GCC did document some mapping info. It appears that 0x4010a5 maps to the multiplication assertion, along with the printf at line 15:

 /house/wcohen/current/202207youarehere/get rid of.c (mtime: 0, size: 0)
…
	18:1        	0   0  0 0x00000000004010a4 <principal+0x54>
	14:3   S    	0   0  0 0x00000000004010a5 <principal+0x55>
	15:3   S    	0   0  0 0x00000000004010a5 <principal+0x55>
	15:3        	0   0  0 0x00000000004010b0 <principal+0x60>
	15:3   	*	0   0  0 0x00000000004010b6 <principal+0x66>

Optimization impacts line info

The line info included into compiled binaries is useful when pinpointing the place in code the processor is. However, optimization can have an effect on the road info, and what you see when debugging the code.

When utilizing a debugger, count on that boundaries between traces of code are fuzzy, and the debugger is more likely to bounce between them when stepping by the code. An instruction would possibly map to a number of traces of supply code, however the debugger might solely reviews one. The compiler might totally get rid of directions related to a line of code, and it could or might not embrace line mapping info. The line info generated by the compiler is useful, however take into account that one thing may be misplaced in translation.

Most Popular

To Top