点击上方蓝字 江湖评谈设为关注/星标
前言
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.py
import 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_writeBytes
Fucntion 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, 0xc
Function 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: invokeStaticMainWithArgs
Function Name: JavaMain
Function Name: ThreadJavaMain
Function Name: start_thread
__clone3 :
以上分为Function Name意即函数名,Function ASM调用了汇编的地方没有函数名,直接打印出汇编调用的地址及其上下文。Java解释器部分是汇编代码构成的,汇编代码出了名的难看。所以这里直接把它打印出来,更直观观察和查看其原理。
Java_java_io_FileOutputStream_writeBytes函数其源码如下:
//java/jdk/src/java.base/share/native/libjava/FileOutputStream.c:69
JNIEXPORT void JNICALL
Java_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.py
import 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 $pc
libc.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编译器)上。
往期精彩回顾