1. 汇编寄存器调用约定
1.1 汇编101
看看下面的汇编片段:
pushq %rbx
subq $0x228, %rsp
movq %rdi, %rbx
在这段汇编代码中有三个操作码pushq
、subq
和movq
。
以%
开头的是寄存器:rbx
, rsp
, rdi
和 rbp
。
还有一个以$
开头的0x228
,$
表示这是一个绝对数。
1.2 x86_64 vs ARM64
作为苹果平台的开发者,接触到的2个主要的架构:x86_64
和ARM64
。x86_64
是一个64位架构,多数用在macOS计算机中;ARM64
架构用于移动设备。ARM
强调的是节约能量,减少了很多指令集来帮助节省能耗。
1.3 x86_64寄存器的调用约定
程序运行时,CPU利用一组寄存器来操作数据。它们离CPU很近,所以CPU可以非常快地访问它们。多数指令会用到一个或多个寄存器来进行操作。在x64
(以下作为x86_64的简称)中有16中寄存器:RAX
, RBX
, RCX
, RDX
, RDI
, RSI
, RSP
, RBP
以及R8
~R15
。
NSString *name = @"Zoltan";
NSLog(@"Hello world, I am %@. I'm %d, and I live in %@.", name, 30, @"my father's basement");
我们看到有4个参数传递给NSLog
。有一个指针引用的临时变量,其他是常量。转换成汇编之后,我们不关心名称与变量,只关心在内存中的位置。
在x64
汇编中,当函数调用时,下面这些寄存器被用作参数,需要好好记住,按顺序为:RDI
、RSI
、RDX
、RCX
、R8
、R9
。如果超过6个参数,剩下的参数会放在栈上。
那么刚刚的OC代码大概就是像这样
RDI = @"Hello world, I am %@. I'm %d, and I live in %@.";
RSI = @"Zoltan";
RDX = 30;
RCX = @"my father's basement";
NSLog(RDI, RSI, RDX, RCX);
但是,当函数序言执行完的时候,这些寄存器中的值很可能会变化。所以,在调试的过程中,你可能希望不跳过函数序言,以便访问这些寄存器。
//在~/.lldbinit中添加
settings set target.skip-prologue false
1.4 Objective-C和寄存器
OC是动态语言,执行方法时实际上调用的是objc_msgSend
。
[UIApplication sharedApplication];
//实际
id UIApplicationClass = [UIApplication class];
objc_msgSend(UIApplicationClass, "sharedApplication");
NSString *helloWorldString = [@"Can't Sleep; "
stringByAppendingString:@"Clowns will eat me"];
//实际
NSString *helloWorldString;
helloWorldString = objc_msgSend(@"Can't Sleep; ", "stringByAppendingString:", @"Clowns will eat me");
在工程中设置一个viewDidLoad
的断点,输入register read
会打印当前主要的寄存器。
(lldb) register read
General Purpose Registers:
rax = 0x0000000100009eb8 (void *)0x000000010000a000: _TtC9Registers14ViewController
rbx = 0x0000600003004b40
rcx = 0x0000000000000000
rdx = 0x0000000000000000
rdi = 0x0000600003004b40
rsi = 0x00007fff7a634d58
rbp = 0x00007ffeefbfe3d0
rsp = 0x00007ffeefbfe398
r8 = 0x0000000000000010
r9 = 0x0000000000000000
r10 = 0x00007fff9141c5b8 (void *)0x000000950000001b
r11 = 0x0000000100134532 libMainThreadChecker.dylib`__trampolines + 11975
r12 = 0x000000010055c410
r13 = 0x0000600003004b40
r14 = 0x0000000000000068
r15 = 0x000000010055c410
rip = 0x00007fff3619118e AppKit`-[NSViewController viewDidLoad]
rflags = 0x0000000000000216
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
-[NSViewController viewDidLoad]
会被翻译成
RDI = NSViewControllerInstance
RSI = "viewDidLoad"
objc_msgSend(RDI, RSI)
来打印一下RDI
和RSI
寄存器,就是objc_msgSend
的两个参数。
(lldb) po $rdi
<Registers.ViewController: 0x600003004b40>
//打印
(lldb) po $rsi
140735246716248
(lldb) po (char *)$rsi
"viewDidLoad"
(lldb) po (SEL)$rsi
"viewDidLoad"
1.5 Swift和寄存器
在Swift中调试比OC中困难一些。
- Swift调试上下文中不支持寄存器访问,所以要靠OC环境来翻译
expression -l objc -O --
🙂。还好register read
是支持的。 - Swift不像OC一样那么动态,有时候你可以直接把Swift看成C。当你拿到内存地址后,你需要类型转换,否则Swift调试上下文怎么解释这个地址。
在Swift中,就没有objc_msgSend
了。
func executeLotsOfArguments(one: Int, two: Int, three: Int,
four: Int, five: Int, six: Int,
seven: Int, eight: Int, nine: Int,
ten: Int) -> String {
print("arguments are: \(one), \(two), \(three),
\(four), \(five), \(six), \(seven),
\(eight), \(nine), \(ten)")
return "10params"
}
override func viewDidLoad() {
super.viewDidLoad()
let _ = executeLotsOfArguments(one: 1, two: 2, three: 3, four: 4,
five: 5, six: 6, seven: 7,
eight: 8, nine: 9, ten: 10)
}
我们在函数声明的地方设置一个断点,并将结果用十进制打印register read -f d
。我们将会看到RDI
、RSI
、RDX
、RCX
、R8
、R9
中是函数调用的前6个参数,其他的被放到了栈上。
(lldb) register read -f d
General Purpose Registers:
rax = 105553166599440
rbx = 105553166599440
rcx = 4
rdx = 3
rdi = 1
rsi = 2
rbp = 140732920751056
rsp = 140732920750936
r8 = 5
r9 = 6
r10 = 4294981360 Registers`Registers.ViewController.executeLotsOfArguments(one: Swift.Int, two: Swift.Int, three: Swift.Int, four: Swift.Int, five: Swift.Int, six: Swift.Int, seven: Swift.Int, eight: Swift.Int, nine: Swift.Int, ten: Swift.Int) -> Swift.String at ViewController.swift:41
r11 = 140734100271100 AppKit`-[NSResponder release]
r12 = 4302351648
r13 = 105553166599440
r14 = 104
r15 = 4302351648
rip = 4294981360 Registers`Registers.ViewController.executeLotsOfArguments(one: Swift.Int, two: Swift.Int, three: Swift.Int, four: Swift.Int, five: Swift.Int, six: Swift.Int, seven: Swift.Int, eight: Swift.Int, nine: Swift.Int, ten: Swift.Int) -> Swift.String at ViewController.swift:41
rflags = 518
cs = 43
fs = 0
gs = 0
寄存器还有一个简单的访问方式。arg1表示
RDI
中的参数,$arg2表示RSI
中的参数。而且这个访问方式可以在x86_64和ARM64具有相同的效果。
1.6 RAX,返回值寄存器
参数的寄存器有了,那么返回值呢?答案是RAX
寄存器。
我们在executeLotsOfArguments
调用出设置一个断点,然后点击Step over
。我们可以看到10个参数的打印,再读取RAX
寄存器。
arguments are: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
(lldb) register read rax
rax = 0x736d617261703031
(lldb) p/c 0x736d617261703031
(Int) $R0 = 10params
1.7 修改寄存器中的值
@interface ViewController ()
@property (nonatomic, strong) UILabel *label;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.label = [[UILabel alloc] init];
self.label.textColor = [UIColor blackColor];
self.label.text = @"Normal";
[self.label sizeToFit];
self.label.center = self.view.center;
[self.view addSubview:self.label];
}
@end
我们在LLDB中输入
//打印@"Yay! Debugging"的地址
lldb) p/x @"Yay! Debugging"
(__NSCFString *) $0 = 0x00006000008a7300 @"Yay! Debugging"
//在-[UILabel setText:]设置断点
(lldb) b -[UILabel setText:]
Breakpoint 2: where = UIKitCore`-[UILabel setText:], address = 0x00007fff484bec59
//命中后添加自动执行的命令
(lldb) breakpoint command add
Enter your debugger command(s). Type 'DONE' to end.
//把RDX寄存器中的值(第三个参数)改为我们@"Yay! Debugging"的地址
> po $rdx = 0x00006000008a7300
> continue
> DONE
(lldb) continue
看到了么,UILabel
中的值被我们修改了~
1.7 寄存器和SDK
在UIView
、UIViewController
或UITableViewCell
设置了IBActions
,方法名可能叫什么什么tapped
。
(lldb) rb View(Controller|Cell)?\s(?i).*tapped
但是你在用别人做的SDK
,别人的习惯和你不一样怎么办?还好我们知道IBActions
的原理,-[UIControl sendAction:to:forEvent:]
一定会被调用。
(lldb) breakpoint set -n "-[UIControl sendAction:to:forEvent:]"
(lldb) po (char *)$rdx
"likeAction"
(lldb) po (char *)$rcx
<ViewController: 0x7fc038701750>
2. 汇编与内存
2.1 设置Intel汇编
有两种方式来展示汇编。一种是LLDB默认的AT&T
汇编:
opcode source destination
//比如
movq $0x78, %rax
下面我们使用Intel
汇编,~/.lldbinit
中配置:
settings set target.x86-disassembly-flavor intel
settings set target.skip-prologue false
Intel
汇编会交换源和目标的位置,并移除%
和$
。
mov rax, 0x78
在添加一个cpx
指令,用OC调试上下文打印地址。
command alias -H "Print value in ObjC context in hexadecimal" -h "Print in hex" -- cpx expression -f x -l objc --
2.2 RIP寄存器(指令指针寄存器)
运行起来,进入断点。
我们看一下这个指针值
(lldb) cpx $rip
(unsigned long) $0 = 0x0000000100006830
我们来看看aGoodMethod
的地址,可以看到对应的地址是0x0000000100006a60
。我们再把当前的RIP
寄存器数据改成0x0000000100006a60
,然后继续运行,我们就可以看到打印aGoodMethod()
。
也就是说,我们在aBadMethod
开始时,通过修改RIP
寄存器的值,让程序执行了aGoodMethod
函数。
(lldb) image lookup -rn ^Registers.*aGoodMethod
1 match found in /Users/ycpeng/Library/Developer/Xcode/DerivedData/Registers-chfyhndrmwptoxhbdijdwvtafrix/Build/Products/Debug/Registers.app/Contents/MacOS/Registers:
Address: Registers[0x0000000100006a60] (Registers.__TEXT.__text + 21088)
Summary: Registers`Registers.AppDelegate.aGoodMethod() -> () at AppDelegate.swift:38
(lldb) register write rip 0x0000000100006a60
aGoodMethod()
2.3 寄存器不同的比特长度输出
2.4 查看内存
我们再试试其他的memory read
可以读取内存,-f
是格式化参数,这里i
是指汇编指令格式,-c
表示需要打印指令条数,1
表示只打印一条。打印0x55
,我们可以看到这是push rbp
指令。
(lldb) cpx $rip
(unsigned long) $0 = 0x0000000100006830
(lldb) memory read -fi -c1 0x0000000100006830
-> 0x100006830: 55 push rbp
(lldb) expression -f i -l objc -- 0x55
(int) $1 = 55 push rbp
我们现在在Swift调试上下文,可以通过调用栈切换到OC调试上下文,用更简单的命令进行打印p/i 0x55
。
我们打印10条命令来看看,可以发现指令的长度是可变的。
(lldb) memory read -fi -c10 0x0000000100006830
-> 0x100006830: 55 push rbp
0x100006831: 48 89 e5 mov rbp, rsp
0x100006834: 41 55 push r13
0x100006836: 48 81 ec c8 00 00 00 sub rsp, 0xc8
0x10000683d: 48 c7 45 f0 00 00 00 00 mov qword ptr [rbp - 0x10], 0x0
0x100006845: 0f 57 c0 xorps xmm0, xmm0
0x100006848: 0f 29 45 e0 movaps xmmword ptr [rbp - 0x20], xmm0
0x10000684c: 4c 89 6d f0 mov qword ptr [rbp - 0x10], r13
0x100006850: 4c 8b 2d f1 27 00 00 mov r13, qword ptr [rip + 0x27f1] ; (void *)0x00007fff8d6229d8: type metadata for Any
0x100006857: 49 83 c5 08 add r13, 0x8
第一条指令只有1字节(0x55),接下来的指令是3字节。
我们输入一下第二条指令,你会发现完全都不一样?其实这个是一个大小端问题。x64 ARM
的结构都是小端的。
(lldb) p/i 0x4889e5
(int) $5 = e5 89 in eax, 0x89
- 大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
- 小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
所以,第二条指令的地址应该反过来,即0xe58948
,现在就对上了。
(lldb) p/i 0xe58948
(int) $6 = 48 89 e5 mov rbp, rsp
3. 汇编与栈
3.1又见栈
栈是从高地址开始的,由操作系统核心决定。栈的长度是有限的,而且地址大小是向下增长的,从高位地址向低位地址增长。
如果到达系统核心给的最大长度,或者跑到堆区的话,栈就溢出了。这个是个致命的错误,通常称之为栈溢出。
3.2 栈指针和基址指针寄存器
有两个非常重要的指针寄存器RSP
和RBP
。
-
RSP
是栈指针寄存器,总是指向某个线程的栈顶。栈顶是向下移动的,从高位地址向低位地址移动。 -
RBP
是基址指针寄存器。在函数调用时,有多作用。在函数或方法的执行过程中,程序利用RBP
寄存器和偏移量来访问临时变量或函数的参数。在函数序言中,RBP
的值被写入到RBP
寄存器当中。 -
RBP
指向栈帧的基地址,RSP
指向栈顶。
当程序序言执行完后,RBP
会指向低一个栈帧大小的之前的RBP
。
- 为什么它们这么重要呢
当你在调试的时候,调试信息参考RBP
和偏移量来获取变量。当程序发布时编译后会进行优化,调试信息会被移除。但你仍然可以通过栈指针和基址指针来定位这些信息。
3.2 栈相关的操作码
-
push
当int
、OC实例、Swift类或一个引用需要保存到栈上时,需要用到push
操作码。push
操作会让栈指针向低位移动,然后将值存储到内容地址中,新的RSP
会指向这个地址。
push
操作后,最近入栈的地址会被RSP
所指向。之前值的地址,将会由RSP
加上最近入栈的值的大小所得到的——通常在64位架构上是8字节。伪代码:push 0x5 RSP = RSP - 0x8 *RSP = 0x5
-
pop
pop
是push
相反的操作。pop
把RSP
中的值取出来,保存到目标地址中。之后RSP
或增加0x8
。假设RSP
把值存到RDX
中,伪代码:pop rdx RDX = *RSP RSP = RSP + 0x8
-
call
call
是用来执行函数的。call
会将函数执行完返回的地址入栈,在跳转到函数中。想想一个函数地址为0x7fffb34df410
,执行call
的伪代码0x7fffb34de913 <+227>: call 0x7fffb34df410 0x7fffb34de918 <+232>: mov edx, eax
当命令执行的时候,
RIP
寄存器会先增加,然后命令再执行。当call
执行的时候,RIP
会增加到0x7fffb34de918
,然后执行0x7fffb34de913
所指向的命令。RIP
会被压栈,然后RIP
会被设置为0x7fffb34df410
,这个地址的函数将会被执行。RIP = 0x7fffb34de918 RSP = RSP - 0x8 *RSP = RIP RIP = 0x7fffb34df410
ret
ret
是call
相反的操作。它会出栈栈顶的内容,就是call
操作入栈的返回地址。然后将RIP
设置为这个地址,因此执行返回到函数调用的地方。
3.3 实际观察RBP
和RSP
调用StackWalkthrough
方法,我们来看一下这个汇编函数怎么执行。
StackWalkthrough(5);
//StackWalkthrough.h
void StackWalkthrough(int x);
//StackWalkthrough.s
.globl _StackWalkthrough
_StackWalkthrough:
push %rbp // Push contents of RBP onto the stack (*RSP = RBP, RSP decreases)
movq %rsp, %rbp // RBP = RSP
movq $0x0, %rdx // RDX = 0
movq %rdi, %rdx // RDX = RDI
push %rdx // Push contents of RDX onto the stack (*RSP = RDX, RSP decreases)
movq $0x0, %rdx // RDX = 0
pop %rdx // Pop top of stack into RDX (RDX = *RSP, RSP increases)
pop %rbp // Pop top of stack into RBP (RBP = *RSP, RSP increases)
ret // Return from function (RIP = *RSP, RSP increases)
断点断在了StackWalkthrough
调用的地方,call
操作命令!
我们来打印一下。先定义一个别名dumpreg
,方便打印这几个寄存器。我们可以看到RDI
中存储的我们的第一个参数5
。
(lldb) command alias dumpreg register read rsp rbp rdi rdx
(lldb) dumpreg
rsp = 0x00007ffeefbfe500
rbp = 0x00007ffeefbfe530
rdi = 0x0000000000000005
rdx = 0x0000000000000018
step into
之后,RSP
变小了0x8
,里面存储的值就是之前函数的返回地址,理论上这个值应该是我们截图里面的下一条命令的地址0x100002f98
,我们也可以输入x/gx $rsp
验证一下。
//0x100002f93 <+99>: call 0x100001750 ; StackWalkthrough
//0x100002f98 <+104>: add rsp, 0x30
(lldb) dumpreg
rsp = 0x00007ffeefbfe4f8 // 0x00007ffeefbfe500 - 0x8
rbp = 0x00007ffeefbfe530
rdi = 0x0000000000000005
rdx = 0x0000000000000018
(lldb) x/gx $rsp
0x7ffeefbfe4f8: 0x0000000100002f98
我们看到0x7ffeefbfe4f8
指向的就是我们的0x100002f98
,即call
命令下面的那条命令的地址。
再执行一步push %rbp
,可以看到我们的RSP
已经等于RBP
的值了。
// push %rbp
(lldb) dumpreg
rsp = 0x00007ffeefbfe4f0 // 0x00007ffeefbfe4f8 - 0x8
rbp = 0x00007ffeefbfe530
rdi = 0x0000000000000005
rdx = 0x0000000000000018
(lldb) x/gx $rsp
0x7ffeefbfe4f0: 0x00007ffeefbfe530
x/gx
中x
表示memory read
,/gx
表示用16进制格式化。
下一步,我们可以看到现在RBP
中的值和RSP
的值相等了。
// 0x100001751 <+1>: mov rbp, rsp
(lldb) dumpreg
rsp = 0x00007ffeefbfe4f0
rbp = 0x00007ffeefbfe4f0
rdi = 0x0000000000000005
rdx = 0x0000000000000018
下一步,RDX
寄存器中的值设置为0。
// 0x100001754 <+4>: mov rdx, 0x0
(lldb) dumpreg
rsp = 0x00007ffeefbfe4f0
rbp = 0x00007ffeefbfe4f0
rdi = 0x0000000000000005
rdx = 0x0000000000000000
下一步,RDX
中的值就和RDI
中的值相等。
// 0x10000175b <+11>: mov rdx, rdi
(lldb) dumpreg
rsp = 0x00007ffeefbfe4f0
rbp = 0x00007ffeefbfe4f0
rdi = 0x0000000000000005
rdx = 0x0000000000000005
下一步,把RDX
中的值入栈。我们看到RSP
地址向下移动了,我们打印它里面的值x/gx $rsp
,发现就是0x5
。
// 0x10000175e <+14>: push rdx
(lldb) dumpreg
rsp = 0x00007ffeefbfe4e8 // 0x00007ffeefbfe4f0 - 0x8
rbp = 0x00007ffeefbfe4f0
rdi = 0x0000000000000005
rdx = 0x0000000000000005
(lldb) x/gx $rsp
0x7ffeefbfe4e8: 0x0000000000000005
下一步,RDX
寄存器中的值设置为0。
// 0x10000175f <+15>: mov rdx, 0x0
(lldb) dumpreg
rsp = 0x00007ffeefbfe4e8
rbp = 0x00007ffeefbfe4f0
rdi = 0x0000000000000005
rdx = 0x0000000000000000
下一步,RDX
寄存器中的值设置为5,和入栈时一样。即出栈前RSP
的地址0x7ffeefbfe4e8
指向的0x5
。同时RSP
地址向上移动。
// 0x100001766 <+22>: pop rdx
(lldb) dumpreg
rsp = 0x00007ffeefbfe4f0 // 0x00007ffeefbfe4e8 + 0x8
rbp = 0x00007ffeefbfe4f0
rdi = 0x0000000000000005
rdx = 0x0000000000000005
下一步,RBP
中的值恢复到刚进入函数入栈时的原始值0x00007ffeefbfe530
。即出栈前RSP
的地址0x7ffeefbfe4f0
指向的0x00007ffeefbfe530
。同时RSP
地址向上移动。
// 0x100001767 <+23>: pop rbp
(lldb) dumpreg
rsp = 0x00007ffeefbfe4f8 // 0x00007ffeefbfe4f0 + 0x8
rbp = 0x00007ffeefbfe530
rdi = 0x0000000000000005
rdx = 0x0000000000000005
最后ret
,我们可以看到RSP
已经恢复到了call
调用前的值0x00007ffeefbfe500
。同时,之前RSP
中0x00007ffeefbfe4f8
指向的0x0000000100002f98
,ret
执行过后,我们的RIP
也指向了0x0000000100002f98
这个地址。
(lldb) x/gx $rsp
0x7ffeefbfe4f8: 0x0000000100002f98
// 0x100001768 <+24>: ret
(lldb) dumpreg
rsp = 0x00007ffeefbfe500 // 0x00007ffeefbfe4f8 + 0x8
rbp = 0x00007ffeefbfe530
rdi = 0x0000000000000005
rdx = 0x0000000000000005
// 0x100002f93 <+99>: call 0x100001750 ; StackWalkthrough
// 0x100002f98 <+104>: add rsp, 0x30
(lldb) cpx $rip
(unsigned long) $5 = 0x0000000100002f98
3.4 栈和超过6个的参数
之前我们提到RDI
、RSI
、RDX
、RCX
、R8
和R9
会依次保存函数的前6个参数,当超过6个参数时,就需要用到栈了。
64位的架构每个寄存器只能存储8字节的数据,如果传入的结构体需要超过8字节,它也需要被存到栈上。
我们还是在下面这个函数中上下一个断点。
let _ = executeLotsOfArguments(one: 1, two: 2, three: 3, four: 4,
five: 5, six: 6, seven: 7,
eight: 8, nine: 9, ten: 10)
在
mov edi, 0x1
和断点之间,前6个参数已经放到对应的寄存器里面了。那7~10这个几个参数呢?它们是这样放到栈上的:
// 0x10000305b <+123>: mov rsi, rsp
0x10000305e <+126>: mov qword ptr [rsi + 0x18], 0xa
0x100003066 <+134>: mov qword ptr [rsi + 0x10], 0x9
0x10000306e <+142>: mov qword ptr [rsi + 0x8], 0x8
0x100003076 <+150>: mov qword ptr [rsi], 0x7
第一行的意思是,把
0xa
放到rsi
中地址加0x18
的地方;
第二行的意思是,把0x9
放到rsi
中地址加0x10
的地方;
第三行的意思是,把0x8
放到rsi
中地址加0x8
的地方;
第四行的意思是,把0x7
放到rsi
中地址的地方;
虽然这里使用的是rsi
,但看上面一句我们就可以知道,实际上rsi
中的值就是rsp
中的值,实际还是栈指针。
我们把数据放到栈上了,但是我们并没有使用push
操作。为什么呢?
我们已经知道了call
会入栈返回地址,函数序言会入栈基地址,基地址指针会被设置到栈指针中。但我们暂时还不知道的是,编译器实际会在栈上留一些暂存空间(scratch space)。就是说,编译器分配了一些空间给函数中的临时变量。如果你在汇编代码中的程序序言中查找sub rsp, VALUE
就会发现它。
相比于一大堆
push
操作,编译器提前分配了一些空间,方便数据的存储。单独的push
操作会产生更多的RSP
寄存器的写入,效率会比较低。
3.5 栈和调试信息
之前我们知道了栈上存储了临时变量,但是调试器是如何知道这些变量的名字的呢?
我们设置一个符号断点。
当断点断住时,one
中的值并不是0x1
,而且还像个随机数?答案在Registers应用程序在调试build时生成的DWARF
调试信息中。我们可以dump
这个信息来辅助我们查看one
这个变量在内存中的值。
(lldb) image dump symfile Registers
//搜索"one"
0x7fbae7c79550: Function{0x300000246}, mangled = $s9Registers14ViewControllerC22executeLotsOfArguments3one3two5three4four4five3six5seven5eight4nine3tenSSSi_S9itF, type_uid = 0x300000246
0x7fbae7c79588: Block{0x300000246}, ranges = [0x100003160-0x10000384c)
0x7fbae7dea8d8: Variable{0x300000263}, name = "one", type = {2110000003000000} 0x00007fbae551e9a0 (Swift.Int), scope = parameter, decl = ViewController.swift:41, location = DW_OP_fbreg(-40)
基于上面的结果,大致可以看出这个方法是Registers.ViewController.executeLotsOfArguments
,其中one
是Swift.Int
类型,他可以在DW_OP_fbreg(-40)
这个位置找到。大概的意思就是RBP - 40
或十六进制RBP - 0x28
。
这是一个非常重要的信息。它告诉调试器,当变量在作用范围之内时,one
这个变量值总是可以在内存地址中找到的。但你可能会想为啥不用RDI
,它就是用来保存第一个参数的呀。因为RDI
可能在后面的执行中被复用,存在栈上会更安全。
si
是thread step-inst
,执行一步汇编代码。
在断点的上面,我们看到之前这个位置被赋值为0
了,断点处打印为0
;执行过这个指令后,one
被赋值为RDI
寄存器中的值,所以打印了1
。
0x100003182 <+34>: mov qword ptr [rbp - 0x28], 0x0
3.6 栈探究的小贴士
在栈探究过程中有一些非常重要的信息需要牢记。
假设你在一个函数中,函数已经执行完函数序言,对于x64
汇编来说有一些一定是对的信息:
-
RBP
将会指向这个函数栈帧的开始位置。 -
*RBP
包含的的地址是前一个栈帧的开始位置。(可以用x/gx $rbp
来查看) -
*(RBP + 0x8)
将会指向在栈跟踪的前一个函数的返回地址(可以用x/gx '$rbp + 0x8'
来查看)。 -
*(RBP + 0x10)
如果有第7个参数的话,将会指向第7个参数。 -
*(RBP + 0x18)
如果有第8个参数的话,将会指向第8个参数。 -
*(RBP + 0x20)
如果有第9个参数的话,将会指向第9个参数。 -
*(RBP + 0x28)
如果有第10个参数的话,将会指向第10个参数。 -
RBP - X
,X
是0x8
的倍数,将会指向函数的临时变量。