If you are debugging a program with symbols and source available, you
can see the actual source code. The easiest way to do this is with the
list command. Calling list multiple times will
advance the section of the source code displayed.
You can also specify funtions, line numbers, and files to list. The format is essentially the same as that used for breakpoint linespecs.
You can also view the assembly instructions for the program, using
disassemble. With no arguments, it will disassemble the
current function. If we break in f and then run
disassemble, we might see something like (for the AMD64
platform):
(gdb) disassemble
Dump of assembler code for function f:
0x0000559f1a0c5163 <+0>: endbr64
0x0000559f1a0c5167 <+4>: push %rbp
0x0000559f1a0c5168 <+5>: mov %rsp,%rbp
=> 0x0000559f1a0c516b <+8>: lea 0xe99(%rip),%rax # 0x559f1a0c600b
0x0000559f1a0c5172 <+15>: mov %rax,%rdi
0x0000559f1a0c5175 <+18>: call 0x559f1a0c5050 <puts@plt>
0x0000559f1a0c517a <+23>: mov $0x0,%eax
0x0000559f1a0c517f <+28>: call 0x559f1a0c5149 <g>
0x0000559f1a0c5184 <+33>: nop
0x0000559f1a0c5185 <+34>: pop %rbp
0x0000559f1a0c5186 <+35>: ret
End of assembler dump.
The arrow indicates the next instruction that the CPU will execute. Let’s take a closer look at this line:
=> 0x0000559f1a0c516b <+8>: lea 0xe99(%rip),%rax # 0x559f1a0c600b
The first value we see is 0x0000559f1a0c516b; this is
the address of the instruction, and is in the code segment of the
process. After this we see <+8>, which tells us that
this instruction begins 8 bytes after the beginning of the function.
Next is the actual instruction, as assembly, which is
lea 0xe99(%rip),%rax. This is a “load effective address”,
which allows us to work with memory addresses (pointers). The comment at
the end of the line tells us what the value will be: %rip
at the time the instruction is executed plus 0xe99. Since
the instruction is at 0x0000559f1a0c516b, once it is
executing the instruction pointer will be advanced to
0x0000559f1a0c5172. If we add 0xe99 to this,
we get 0x559f1a0c600b.
We can do more with disassemble, though. If we want to
see the actual bytecode, we can add the “raw” option with
disassemble/r:
(gdb) disassemble/r
Dump of assembler code for function f:
0x0000559f1a0c5163 <+0>: f3 0f 1e fa endbr64
0x0000559f1a0c5167 <+4>: 55 push %rbp
0x0000559f1a0c5168 <+5>: 48 89 e5 mov %rsp,%rbp
=> 0x0000559f1a0c516b <+8>: 48 8d 05 99 0e 00 00 lea 0xe99(%rip),%rax # 0x559f1a0c600b
0x0000559f1a0c5172 <+15>: 48 89 c7 mov %rax,%rdi
0x0000559f1a0c5175 <+18>: e8 d6 fe ff ff call 0x559f1a0c5050 <puts@plt>
0x0000559f1a0c517a <+23>: b8 00 00 00 00 mov $0x0,%eax
0x0000559f1a0c517f <+28>: e8 c5 ff ff ff call 0x559f1a0c5149 <g>
0x0000559f1a0c5184 <+33>: 90 nop
0x0000559f1a0c5185 <+34>: 5d pop %rbp
0x0000559f1a0c5186 <+35>: c3 ret
End of assembler dump.
Certain common instructions, like push %rbp, are
single-byte instructions (55).
We can also see the corresponding source code, if available, with the
“source” options dissamble/s:
(gdb) disassemble/s
Dump of assembler code for function f:
example.c:
7 void f() {
0x0000559f1a0c5163 <+0>: endbr64
0x0000559f1a0c5167 <+4>: push %rbp
0x0000559f1a0c5168 <+5>: mov %rsp,%rbp
8 printf("in f()\n");
=> 0x0000559f1a0c516b <+8>: lea 0xe99(%rip),%rax # 0x559f1a0c600b
0x0000559f1a0c5172 <+15>: mov %rax,%rdi
0x0000559f1a0c5175 <+18>: call 0x559f1a0c5050 <puts@plt>
9 g();
0x0000559f1a0c517a <+23>: mov $0x0,%eax
0x0000559f1a0c517f <+28>: call 0x559f1a0c5149 <g>
10 }
0x0000559f1a0c5184 <+33>: nop
0x0000559f1a0c5185 <+34>: pop %rbp
0x0000559f1a0c5186 <+35>: ret
End of assembler dump.
Here we see lines of source code (including line numbers), followed
by the instructions involved in executing them. You can even combine the
raw and source options with disassemble/rs, which does what
you would expect.
We will see another way to display assembly instructions later.
disassemble, disas