In my previous article: Disassembly with GDB, I wrote about inspecting disassembly with the GNU debugger. This time, I’ll be covering another popular debugger: LLDB. Unlike GDB, which I use daily at work, LLDB is something I use rarely; writing this article was a learning experience for me.
Beyond the debugger itself, there are a couple of other differences in this article:
I’ll be using Aarch64 Linux instead of x86-x64 Linux;
The sample C program will be compiled with -O1 optimization.
Sample C Program
The sample C program is exactly the same as in the article on GDB:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char **argv) {
if (argc != 3) exit(EXIT_FAILURE);
const char *s1 = argv[1], *s2 = argv[2];
const size_t len = strlen(s1);
if (len != strlen(s2)) exit(EXIT_FAILURE);
size_t distance = 0;
for (size_t i = 0; i < len; i++) {
if (s1[i] != s2[i]) {
distance++;
}
}
printf("%ld\n", distance);
return EXIT_SUCCESS;
}
We will compile it with:
clang -g -O1 -o main main.c
Starting the Debugger
To start the debugger, we’ll use the lldb command. Note how to pass the arguments we use two dashes unlike --args
with gdb:
lldb -- main abc abd
(lldb) target create "main"
Current executable set to '[...]/lldb/main' (aarch64).
(lldb) settings set -- target.run-args "abc" "abd"
(lldb)
Disassemble Command
Like GDB, LLDB has the dissasemble command. Its main function is the same, but the usage is somewhat different. Make sure to type help disassemble
from lldb prompt and familiarize yourself with the supported options.
Here we are going to use -n ( --name) to specify the function we want to disassemble. Also, we’ll add -m (--mixed) to display the source code alongside the assembly - similar to GDB’s /s
option.:
(lldb) disas -n main -m
** 6 int main(int argc, char **argv) {
main`main:
main[0x818] <+0>: stp x29, x30, [sp, #-0x30]!
main[0x81c] <+4>: str x21, [sp, #0x10]
main[0x820] <+8>: stp x20, x19, [sp, #0x20]
main[0x824] <+12>: mov x29, sp
** 7 if (argc != 3) exit(EXIT_FAILURE);
8
main[0x828] <+16>: cmp w0, #0x3
main[0x82c] <+20>: b.ne 0x890 ; <+120> at main.c
** 9 const char *s1 = argv[1], *s2 = argv[2];
main[0x830] <+24>: ldp x19, x20, [x1, #0x8]
** 10 const size_t len = strlen(s1);
11
main[0x834] <+28>: mov x0, x19
main[0x838] <+32>: bl 0x660 ; symbol stub for: strlen
main[0x83c] <+36>: mov x21, x0
** 12 if (len != strlen(s2)) exit(EXIT_FAILURE);
13
14 size_t distance = 0;
main[0x840] <+40>: mov x0, x20
main[0x844] <+44>: bl 0x660 ; symbol stub for: strlen
main[0x848] <+48>: cmp x21, x0
main[0x84c] <+52>: b.ne 0x890 ; <+120> at main.c
main[0x850] <+56>: mov x1, xzr
** 15 for (size_t i = 0; i < len; i++) {
main[0x854] <+60>: cbz x21, 0x870 ; <+88> at main.c:21:5
** 16 if (s1[i] != s2[i]) {
17 distance++;
18 }
main[0x858] <+64>: ldrb w8, [x19], #0x1
main[0x85c] <+68>: ldrb w9, [x20], #0x1
main[0x860] <+72>: cmp w8, w9
main[0x864] <+76>: cinc x1, x1, ne
** 15 for (size_t i = 0; i < len; i++) {
main[0x868] <+80>: subs x21, x21, #0x1
main[0x86c] <+84>: b.ne 0x858 ; <+64> at main.c:16:13
19 }
20
** 21 printf("%ld\n", distance);
22
main[0x870] <+88>: adrp x0, 0
main[0x874] <+92>: add x0, x0, #0x8b0
main[0x878] <+96>: bl 0x6c0 ; symbol stub for: printf
** 23 return EXIT_SUCCESS;
24 }
main[0x87c] <+100>: mov w0, wzr
main[0x880] <+104>: ldp x20, x19, [sp, #0x20]
main[0x884] <+108>: ldr x21, [sp, #0x10]
main[0x888] <+112>: ldp x29, x30, [sp], #0x30
main[0x88c] <+116>: ret
main[0x890] <+120>: mov w0, #0x1 ; =1
main[0x894] <+124>: bl 0x670 ; symbol stub for: exit
(lldb)
Notice that we used disas
instead of full disassemble
command. LLDB performs a shortest unique string match on command names. In fact, we could have used an even shorter form, such as: di
.
(lldb) di -n main -m -c 10
** 6 int main(int argc, char **argv) {
main`main:
main[0x818] <+0>: stp x29, x30, [sp, #-0x30]!
main[0x81c] <+4>: str x21, [sp, #0x10]
main[0x820] <+8>: stp x20, x19, [sp, #0x20]
main[0x824] <+12>: mov x29, sp
** 7 if (argc != 3) exit(EXIT_FAILURE);
8
main[0x828] <+16>: cmp w0, #0x3
main[0x82c] <+20>: b.ne 0x890 ; <+120> at main.c
** 9 const char *s1 = argv[1], *s2 = argv[2];
main[0x830] <+24>: ldp x19, x20, [x1, #0x8]
** 10 const size_t len = strlen(s1);
11
12 if (len != strlen(s2)) exit(EXIT_FAILURE);
main[0x834] <+28>: mov x0, x19
main[0x838] <+32>: bl 0x660 ; symbol stub for: strlen
main[0x83c] <+36>: mov x21, x0
Starting and ending addresses can be provided with -s and -e flags:
(lldb) di -m -s 0x850 -e 0x864
main`main:
main[0x850] <+56>: mov x1, xzr
13
14 size_t distance = 0;
** 15 for (size_t i = 0; i < len; i++) {
main[0x854] <+60>: cbz x21, 0x870 ; <+88> at main.c:21:5
** 16 if (s1[i] != s2[i]) {
17 distance++;
18 }
main[0x858] <+64>: ldrb w8, [x19], #0x1
main[0x85c] <+68>: ldrb w9, [x20], #0x1
main[0x860] <+72>: cmp w8, w9
Single Line Disassembly
Like with the GDB example, let’s put a break point at line 16 and run to it:
(lldb) b 16
Breakpoint 1: where = main`main + 64 at main.c:16:13, address = 0x0000000000000858
(lldb) run
Process 1229 launched: '/home/nemtrif/lldb/main' (aarch64)
Process 1229 stopped
* thread #1, name = 'main', stop reason = breakpoint 1.1
frame #0: 0x0000aaaaaaaa0858 main`main(argc=<unavailable>, argv=<unavailable>) at main.c:16:13
13
14 size_t distance = 0;
15 for (size_t i = 0; i < len; i++) {
-> 16 if (s1[i] != s2[i]) {
17 distance++;
18 }
19 }
(lldb)
We can use the disassemble command with the -l option to inspect the current machine instruction:
(lldb) di -l
-> 6 int main(int argc, char **argv) {
-> 7 if (argc != 3) exit(EXIT_FAILURE);
-> 8
-> 9 const char *s1 = argv[1], *s2 = argv[2];
-> 10 const size_t len = strlen(s1);
-> 11
-> 12 if (len != strlen(s2)) exit(EXIT_FAILURE);
-> 13
-> 14 size_t distance = 0;
-> 15 for (size_t i = 0; i < len; i++) {
-> 16 if (s1[i] != s2[i]) {
main`main:
-> 0xaaaaaaaa0858 <+64>: ldrb w8, [x19], #0x1
Printing the value stored in a register is similar to GDB:
(lldb) p $x0
(unsigned long) 3
Values for multiple registers:
(lldb) register read x0 x1 x2
x0 = 0x0000000000000003
x1 = 0x0000000000000000
x2 = 0x5353454c00646261
(lldb)
And if we want to inspect all registers, there is an easy shortcut:
(lldb) register read --all
General Purpose Registers:
x0 = 0x0000000000000003
x1 = 0x0000000000000000
x2 = 0x5353454c00646261
x3 = 0x2f207c3d4e45504f
x4 = 0x0000000000000018
x5 = 0x0000000000000000
x6 = 0x2e1f7b3c4d444f4e
x7 = 0x7f7f7f7f7f7f7f7f
...
Stepping Through Instructions
An equivalent of GDB disassemble-next-line
setting is to set up a stop hook1 which enables the execution of specific commands whenever the debugger stops. Here, we will add a stop hook that executes two debugger commands: backtrace and disassemble at program counter:
(lldb) target stop-hook add
Enter your stop hook command(s). Type 'DONE' to end.
> bt
> di --pc
Stop hook #1 added.
Now we can run next instruction command to see what happens:
(lldb) ni
* thread #1, name = 'main', stop reason = instruction step over
* frame #0: 0x0000aaaaaaaa085c main`main(argc=<unavailable>, argv=<unavailable>) at main.c:16:22
frame #1: 0x0000fffff7e184c4 libc.so.6`___lldb_unnamed_symbol3097 + 116
frame #2: 0x0000fffff7e18598 libc.so.6`__libc_start_main + 152
frame #3: 0x0000aaaaaaaa0730 main`_start + 48
main`main:
-> 0xaaaaaaaa085c <+68>: ldrb w9, [x20], #0x1
0xaaaaaaaa0860 <+72>: cmp w8, w9
0xaaaaaaaa0864 <+76>: cinc x1, x1, ne
0xaaaaaaaa0868 <+80>: subs x21, x21, #0x1
Process 1229 stopped
* thread #1, name = 'main', stop reason = instruction step over
frame #0: 0x0000aaaaaaaa085c main`main(argc=<unavailable>, argv=<unavailable>) at main.c:16:22
13
14 size_t distance = 0;
15 for (size_t i = 0; i < len; i++) {
-> 16 if (s1[i] != s2[i]) {
17 distance++;
18 }
19 }
As expected, the output consists of three parts: the call stack for the current thread, the disassembly, and the standard output that is displayed when the debugger stops: source code around the current position.
We do not need to use the instruction-level commands to see the disassembly. For instance, the next
command that executes a line of source code will cause the stop hook to get executed as well:
(lldb) n
* thread #1, name = 'main', stop reason = step over
* frame #0: 0x0000aaaaaaaa0868 main`main(argc=<unavailable>, argv=<unavailable>) at main.c:15:26
frame #1: 0x0000fffff7e184c4 libc.so.6`___lldb_unnamed_symbol3097 + 116
frame #2: 0x0000fffff7e18598 libc.so.6`__libc_start_main + 152
frame #3: 0x0000aaaaaaaa0730 main`_start + 48
main`main:
-> 0xaaaaaaaa0868 <+80>: subs x21, x21, #0x1
0xaaaaaaaa086c <+84>: b.ne 0xaaaaaaaa0858 ; <+64> at main.c:16:13
0xaaaaaaaa0870 <+88>: adrp x0, 0
0xaaaaaaaa0874 <+92>: add x0, x0, #0x8b0
Process 1229 stopped
* thread #1, name = 'main', stop reason = step over
frame #0: 0x0000aaaaaaaa0868 main`main(argc=<unavailable>, argv=<unavailable>) at main.c:15:26
12 if (len != strlen(s2)) exit(EXIT_FAILURE);
13
14 size_t distance = 0;
-> 15 for (size_t i = 0; i < len; i++) {
16 if (s1[i] != s2[i]) {
17 distance++;
18 }
This is how we remove the stop hook:
(lldb) target stop-hook delete
Delete all stop hooks?: [Y/n] Y
(lldb)
A LLDB stop hook is a list of LLDB commands or a Python class that runs whenever the debugger stops. To learn more about it, type the following from lldb command prompt: help target stop-hook