(二) LLDB理解汇编

1. 汇编寄存器调用约定

1.1 汇编101

看看下面的汇编片段:

pushq   %rbx
subq    $0x228, %rsp
movq    %rdi, %rbx

在这段汇编代码中有三个操作码pushqsubqmovq
%开头的是寄存器:rbx, rsp, rdirbp
还有一个以$开头的0x228$表示这是一个绝对数。

1.2 x86_64 vs ARM64

作为苹果平台的开发者,接触到的2个主要的架构:x86_64ARM64x86_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汇编中,当函数调用时,下面这些寄存器被用作参数,需要好好记住,按顺序为:RDIRSIRDXRCXR8R9。如果超过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)

来打印一下RDIRSI寄存器,就是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。我们将会看到RDIRSIRDXRCXR8R9中是函数调用的前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

寄存器还有一个简单的访问方式。arg{X}**,X表示参数的序号。比如,**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

UIViewUIViewControllerUITableViewCell设置了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寄存器(指令指针寄存器)


开启汇编显示

运行起来,进入断点。

AppDelegate.aBadMethod

我们看一下这个指针值

(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 寄存器不同的比特长度输出

x位寄存器

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

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 栈指针和基址指针寄存器

有两个非常重要的指针寄存器RSPRBP

  • RSP是栈指针寄存器,总是指向某个线程的栈顶。栈顶是向下移动的,从高位地址向低位地址移动。
  • RBP是基址指针寄存器。在函数调用时,有多作用。在函数或方法的执行过程中,程序利用RBP寄存器和偏移量来访问临时变量或函数的参数。在函数序言中,RBP的值被写入到RBP寄存器当中。
  • RBP指向栈帧的基地址,RSP指向栈顶。
RBP

当程序序言执行完后,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
    poppush相反的操作。popRSP中的值取出来,保存到目标地址中。之后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
    retcall相反的操作。它会出栈栈顶的内容,就是call操作入栈的返回地址。然后将RIP设置为这个地址,因此执行返回到函数调用的地方。

3.3 实际观察RBPRSP

调用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操作命令!

StackWalkthrough

我们来打印一下。先定义一个别名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/gxx表示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。同时,之前RSP0x00007ffeefbfe4f8指向的0x0000000100002f98ret执行过后,我们的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个的参数

之前我们提到RDIRSIRDXRCXR8R9会依次保存函数的前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)

executeLotsOfArguments

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就会发现它。

sub rsp, VALUE

相比于一大堆push操作,编译器提前分配了一些空间,方便数据的存储。单独的push操作会产生更多的RSP寄存器的写入,效率会比较低。

3.5 栈和调试信息

之前我们知道了栈上存储了临时变量,但是调试器是如何知道这些变量的名字的呢?

我们设置一个符号断点。

executeLotsOfArguments
变量

当断点断住时,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,其中oneSwift.Int类型,他可以在DW_OP_fbreg(-40)这个位置找到。大概的意思就是RBP - 40或十六进制RBP - 0x28

这是一个非常重要的信息。它告诉调试器,当变量在作用范围之内时,one这个变量值总是可以在内存地址中找到的。但你可能会想为啥不用RDI,它就是用来保存第一个参数的呀。因为RDI可能在后面的执行中被复用,存在栈上会更安全。

one赋值

sithread 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 - XX0x8的倍数,将会指向函数的临时变量。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容