天下第一调试利器LLDB

文摘   2024-09-19 12:51   湖北  

点击上方蓝字 江湖评谈设为关注/星标




前言

LLDB新一代的高性能调试利器,跨平台(linux/macos/安卓/win),跨指令集(x86/x64/Arm/Risc-v/mips等),跨语言(Rust/.NET/C++/Go等)。

远超其它调试器,天下第一的称当之无愧,LLDB是个人目前用的最为爽手的工具,没有之一。下面来体验下。

友情提示:看完本篇,你就是高手了。

语言层面

个人对于LLDB最先搞.NET,然后进入CLR/JIT,后深入了Linux Kernel和Windows Kernel。Linux上有各种指令集版本比如Arm/Risc-V此时也接触了这两者,现在是跨语言,比如Rust编译器Rustc,Java的JVM,Go的runtime等等。

LLDB上几乎领略了计算机最底层全部技术。

比如通过一个LLDB脚本,直击OpenJDK24的JVM内部函数,Java版本:

lldb脚本:

# filename:systemprint.pyimport lldb
def print_stack_function_names(debugger, command, result, internal_dict): target = debugger.GetSelectedTarget() process = target.GetProcess()    thread = process.GetSelectedThread() for frame in thread: function_name = frame.GetFunctionName() or frame.GetSymbol().GetName() function_address= frame.GetPCAddress()        if function_name==None: interpreter = lldb.debugger.GetCommandInterpreter() return_object = lldb.SBCommandReturnObject() interpreter.HandleCommand('di -s '+str(function_address.GetLoadAddress(target)-0x3)+' -c 0x5', return_object) output = return_object.GetOutput(); # debugger.HandleCommand('shell clear') result.PutCString(f"Fucntion ASM:\n{output}") else:           result.PutCString(f"Function  Name:     {function_name}")    result.SetStatus(lldb.eReturnStatusSuccessFinishResult)
def __lldb_init_module(debugger, internal_dict):    debugger.HandleCommand('command script add -f systemprint.print_stack_function_names printstack')

这个脚本的意思是打印出当前堆栈调用的函数名称,如果堆栈的某个函数调用了汇编代码,那么打印出具体的汇编代码的地址以及汇编代码本身。

假如说你要了解Java的System.out.printf从Java源码到JVM到系统内核调用的过程。例子:

public class HelloWorld {    public static void main(String[] args) {        String name = "Alice";        int age = 0x10;        System.out.printf("Name: %s\n", name);        System.out.printf("Age: %d\n", age);    }}

你可以在Java_java_io_FileOutputStream_writeBytes函数下断点,运行此处。导入上面的脚本进行观察。

(lldb)command script import systemprint.py(lldb)printstack

此时,我们就可以愉快的看到整个JVM的调用过程啦:

Function  Name:     Java_java_io_FileOutputStream_writeBytesFucntion ASM:    0x7fffe85467b7: add    bh, bh    0x7fffe85467b9: rol    ch    0x7fffe85467bb: clc        0x7fffe85467bc: ja     0x7fffe8546806    0x7fffe85467be: sub    esp, 0x10    //中间省略部分代码,便于观看Fucntion ASM:    0x7fffe8538cfe: in     al, dx    0x7fffe8538cff: call   rsi    0x7fffe8538d01: mov    rdi, qword ptr [rbp - 0x28]    0x7fffe8538d05: mov    esi, dword ptr [rbp - 0x20]    0x7fffe8538d08: cmp    esi, 0xcFunction  Name:     JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)Function  Name:     os::os_exception_wrapper(void (*)(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*), JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)Function  Name:     JavaCalls::call(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)Function  Name:     ::jni_invoke_static(JNIEnv *, JavaValue *, jobject, JNICallType, jmethodID, JNI_ArgumentPusher *, JavaThread *)Function  Name:     ::jni_CallStaticVoidMethod(JNIEnv *, jclass, jmethodID, ...)Function  Name:     invokeStaticMainWithArgsFunction  Name:     JavaMainFunction  Name:     ThreadJavaMainFunction  Name:     start_threadFunction  Name:     __clone3

以上分为Function Name意即函数名,Function ASM调用了汇编的地方没有函数名,直接打印出汇编调用的地址及其上下文。Java解释器部分是汇编代码构成的,汇编代码出了名的难看。所以这里直接把它打印出来,更直观观察和查看其原理。

Java_java_io_FileOutputStream_writeBytes函数其源码如下:

//java/jdk/src/java.base/share/native/libjava/FileOutputStream.c:69JNIEXPORT void JNICALLJava_java_io_FileOutputStream_writeBytes(JNIEnv *env,    jobject this, jbyteArray bytes, jint off, jint len, jboolean append) {    writeBytes(env, this, bytes, off, len, append, fos_fd);}

我们运行到writeBytes里面去,查看下buf变量的值,它正是printf的Name字符串

(lldb) fr v buf (char *) buf = 0x00007ffff4ffbbf0 "Name: "

系统层面

要看到Java的System.out.printf的系统调用,可以在glibc的write函数下断,它的系统号是1。write函数有多个断点,每次手动查找比较麻烦,我们可以对它进行脚本化处理。

# filename:function.pyimport lldb
def print_stack_function_names(debugger, command, result, internal_dict):
debugger.HandleCommand('b write') interpreter = lldb.debugger.GetCommandInterpreter() return_object = lldb.SBCommandReturnObject() interpreter.HandleCommand('b', return_object) output = return_object.GetOutput(); glibcindex=output.find('__GI___libc_write')
debugger.HandleCommand('shell clear') address=output[(glibcindex+0x2D):(glibcindex+63)] debugger.HandleCommand('br del --force')    debugger.HandleCommand('b '+address) target = debugger.GetSelectedTarget() process = target.GetProcess() thread = process.GetSelectedThread()
for frame in thread: function_name = frame.GetFunctionName() or frame.GetSymbol().GetName()        result.PutCString(f"Function: {function_name}")    result.SetStatus(lldb.eReturnStatusSuccessFinishResult)    def __lldb_init_module(debugger, internal_dict):    debugger.HandleCommand('command script add -f function.print_stack_function_names write')

那么此时我们的命令如下:

(lldb) printstack(lldb) write

即可看到系统调用的地方,系统号为1的write调用。

(lldb) di -s $pclibc.so.6`:->  0x7ffff7d14870 <+0>:  endbr64     0x7ffff7d14874 <+4>:  mov    eax, dword ptr fs:[0x18]    0x7ffff7d1487c <+12>: test   eax, eax    0x7ffff7d1487e <+14>: jne    0x7ffff7d14890            ; <+32> at write.c:25:1    0x7ffff7d14880 <+16>: mov    eax, 0x1    0x7ffff7d14885 <+21>: syscall     0x7ffff7d14887 <+23>: cmp    rax, -0x1000    0x7ffff7d1488d <+29>: ja     0x7ffff7d148e0            ; <+112> at write.c:26:10    0x7ffff7d1488f <+31>: ret

结尾

以上综合例子运用了三个语言,Java例子,C++的JVM,Python的脚本。可以一眼击穿JVM运行原理。同理可以运用到CLR,Rustc(Rust编译器)上。

往期精彩回顾

.NET AOT之后就安全了吗?

Rust编译器深入

江湖评谈
记录,分享,自由。
 最新文章