汇编和内存

汇编和内存

你已经开了汇编学习的旅程,并且在前几章中你已经学习了汇编调用的一些黑魔法,你现在知道了,当一个函数被调用,他的参数和返回值是如何传递的。但是您还没学到的是将代码加载到内存后如何执行代码。

在本章中,您将探索程序的执行方式。 您将看到一个特殊的寄存器,该寄存器用于告诉处理器应该从何处读取下一条指令,以及不同大小和内存分组如何产生截然不同的结果。

设置英特尔风格汇编体验™

如上一章所述,显示汇编有两种主要方法。 一种类型是AT&T程序集,这个也是LLDB的默认程序集。 这种格式具有以下格式:

opcode(操作码)  source(源)  destination(目的地)

看一个具体的例子

movq  $0x78, %rax

这个操作会将十六进制值 0x78 移动到 RAX 寄存器中。 尽管这种汇编形式对某些人来说很不错,但从现在开始,您将使用英特尔的表示形式。

注意:汇编的选择有点像一场大战,有的人说用Inter的格式,有的说是用AT&T的格式,在StackOverflow中查看以下讨论
选择使用英特尔是因为大家都觉得英特尔在阅读方面更好,但有时在写作方面更差。 由于您正在学习调试,因此大部分时间是在阅读汇编而不是编写汇编。

把以下的两行内容添加进 ~/.lldbinit file 文件底部

  • settings set target.x86-disassembly-flavor intel
  • settings set target.skip-prologue false

第一行告诉LLDB以Intel风格显示x86汇编(32位和64位)。
第二行告诉LLDB不要跳过函数序言。 您在本书的前面已经了解了这一点,从现在开始,请不要跳过序言,因为您将直接从函数的第一条指令检查汇编。

关于函数序言和函数尾声可以再参考这篇文章 注意:在编辑〜/ .lldbinit文件时,请确保您不使用TextEdit之类的程序,因为它将在文件中添加不必要的字符,这可能导致LLDB无法正确解析该文件。 一种简单(尽管很危险)的添加方法是通过如下的Terminal命令:echo“ settings set target.x86-disassembly-flavor intel” >>〜/ .lldbinit。
确保其中有两个“ >>”,否则将覆盖〜/ .lldbinit文件中的所有先前内容。 如果您对终端机不满意,最好的选择是nano(您之前使用过的编辑器)。

英特尔风格将交换源值和目标值,删除’%’和’$’字符以及进行许多其他许多更改。 由于您还不会使用AT&T语法,因此这里就不解释全部的差异,我们只单单学习Inter程序集就够了。

看下面的示例,就是以Inter风格来展示,他看起来比较干净,比较好阅读。

mov  rax, 0x78

同样,这会将十六进制值0x78移到RAX寄存器中。
与前面显示的AT&T风格相比,Intel风格交换了源操作数和目标操作。 现在,目标操作数在源操作数之前。 在进行汇编时,务必始终确定正确的风格,这很重要,因为如果您不清楚要使用的风格,则可能会采取不同的操作。

从现在开始,我们就开始使用Inter的汇编格式了。 如果您看到以$字符开头的数字十六进制常量或以%开头的寄存器,你就要把他们转换成Inter的形式

注:笔者反而觉得AT&T的风格更适合中国汉语的语法,只是前面的 % 和$有些奇怪罢了

创建cpx命令

首先,您将创建自己的LLDB命令,之后会用到。
再次打开〜/ .lldbinit(vim)。 然后将以下内容添加到文件底部:

  • command alias -H “Print value in ObjC context in hexadecimal” -h “Print in hex” – cpx expression -f x -l objc –

cpx是一个便捷命令,您可以使用Objective-C上下文以十六进制格式打印出某些内容。 尤其是在打印出寄存器内容时会用到。
请记住,寄存器在Swift上下文中不可用,因此您需要使用Objective-C上下文。
现在,您已经具有从汇编的角度探讨本章内容所需的工具!

位,字节和其他术语

在开始探索内存之前,您需要了解一些有关内存分组方式的词汇。

  • 位 :可以包含1或0的值称为位。您可以说在64位体系结构中每个地址有64位。很简单。
  • 字节:当8位组合在一起时,它们称为字节。一个字节可以容纳多少个唯一值?您可以通过计算2 ^ 8(从0开始到255的256个值)来确定。

许多信息以字节表示。例如,C语言中 sizeof()函数以字节为单位返回对象的大小。

如果您熟悉ASCII字符编码,您会想起所有ASCII字符都可以保存在一个字节中。
现在是时候看看实际操作中的术语并学习一些技巧。
打开Registers macOS应用程序,您将在本章的资源文件夹中找到该应用程序。接下来,构建并运行该应用程序。一旦运行,请暂停程序并启动LLDB控制台。这将导致使用非Swift调试上下文,因为默认情况下暂停应用程序会带来非Swift上下文。
在LLDB中键入以下内容:

  • p sizeof(‘A’)

这将打印出组成’A’字符所需的字节数

(unsigned long) $0 = 1

然后输入如下命令

  • p/t ‘A’

你会得到

(char) $1 = 0b01000001

这是ASCII中字符A的二进制表示。
显示信息字节的另一种更常见的方法是使用十六进制值。 需要两个十六进制数字以十六进制表示一个信息字节。

然后我们输入以下命令来打印出“ A”的十六进制表示形式:

  • p/x ‘A’

你将会得到

(char) $2 = 0x41

十六进制非常适合查看内存,因为一个十六进制数字恰好代表4位。 因此,如果您有2个十六进制数字,则您有1个字节。 如果您有8个十六进制数字,则您有4个字节。 等等。
这里还有一些适用于您的术语,这些术语在以后的章节中会很有用:

  • Nybble:4位,十六进制单个值
  • Half word:16位或2个字节
  • Word:32位或4个字节
  • Double word or Giant word:64位或8字节。

使用此术语,您将可以探索不同的内存块。

RIP寄存器

当程序执行时,将要执行的代码加载到内存中。 程序中接下来要执行的代码的位置由一个非常重要的寄存器决定:RIP或指令指针寄存器。
让我们来看看实际情况。 再次打开“register demo”应用程序,然后导航到AppDelegate.swift文件。 修改文件,使其包含以下代码:

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

func applicationWillBecomeActive(
    _ notification: Notification) {
    print("\(#function)")
    self.aBadMethod()
}

func aBadMethod() {
    print("\(#function)")
}

func aGoodMethod() {
    print("\(#function)")
}
}

构建并运行该应用程序。 毫无疑问,控制台将会输出 该方法名 applicationWillBecomeActive(_ :) ,然后输出 aBadMethod 。我们在在aBadMethod的开始处创建一个断点:

再次构建并运行。 在aBadMethod的开头命中断点后,导航至Debug \ Debug Workflow \ Always Show Disassembly in Xcode。 现在,您将看到程序的实际汇编!

然后我们在控制台输入以下命令:

cpx $rip

这将使用您先前创建的cpx命令打印出指令指针寄存器。
您会注意到输出LLDB的地址与Xcode中绿线突出显示的地址是一样的:

(unsigned long) $1 = 0x0000000100008910

当然了每个人的电脑上显示的地址是不同的,而且每次执行时候的地址都可能不一样,但是在单次运行中绿线和控制台上显示的肯定是完全一样的。你试着在控制台下打以下命令:

(lldb) image lookup -vrn ^Registers.*aGoodMethod

这是一个很常用的查找命令,其中包含典型的正则表达式参数以及添加的参数 -v ,该参数输出详细信息就像 --verbose
您会看到很多的东西。 在控制台中使用 command + F 来搜索关键字 range = [ 其中范围中的第一个值便是

该地址称为加载地址。 这是此函数在内存中的实际物理地址!这与您在image lookup 命令中看到的常规输出不同,它仅显示函数相对于可执行文件的偏移量,也称为实现偏移量。 寻找函数地址时,区分加载地址和可执行文件中的实现偏移很重要,因为它会有所不同。

将这个新地址复制到范围括号的开头。 对于此特定示例,aGoodMethod的加载地址位于0x0000000100008910。 现在将aGoodMethod的地址指向RIP寄存器。输入以下指令

  • register write rip 0x0000000100008910

单击继续,使用Xcode调试按钮单击“继续”。 请一定用Xcode的按钮来继续,不要是用 continue 命令,因为在修改RIP寄存器并在控制台中继续操作时就会崩溃。按下Xcode继续按钮后,您会看到神奇的事情 -> 未执行aBadMethod(),而是执行了aGoodMethod()。 通过查看控制台日志中的输出来验证这一点。

注意:修改RIP寄存器实际上非常危险。 您需要确RIP寄存器上的数据不会再被使用了,因为新功能将对RIP寄存器做出不正确的假设。 由于aGoodMethod和aBadMethod在功能上非常相似,因此您从一开始就停了下来,并且由于没有对Registers应用程序进行任何修改,因此不必担心。

寄存器和比特分解

如上一章所述,x64具有16个通用寄存器:RDI,RSI,RAX,RDX,RBP,RSP,RCX,RDX,R8,R9,R10,R11,R12,R13,R14和R15。
为了保持与i386的32位架构等以前的体系结构的兼容性,可以将寄存器分为32位,16位或8位值。

对于具有不同体系结构历史的寄存器,寄存器名称中的最前面的字符确定寄存器的大小。 例如,RIP寄存器以R开头,它表示64位。 如果您希望使用等效于RIP寄存器的32位,则可以将R字符换成E,以获得EIP寄存器。

为什么这些有用? 因为使用寄存器时,有时传递到寄存器的值不需要全部使用64位。 例如,考虑布尔数据类型:您真正需要的只是1或0以指示是或否(尽管实际上,布尔值将占用寄存器一个字节)。 基于语言的功能和约束,编译器知道这一点,有时只会将信息写入寄存器的某些部分。
让我们来看看实际情况。
删除Registers项目中的所有断点。 生成并运行项目。 现在,让程序暂停。输入以下内容:

  • register write rdx 0x0123456789ABCDEF

这会将值写入RDX寄存器。让我们停一分钟。 提示:您应该注意,写入寄存器可能会导致程序崩溃,尤其是当您希望写入的寄存器具有某种类型的数据时。 但是你现在是在demo上做调试,所以请不要担心您的程序是否崩溃!
确认此值已成功写入RDX寄存器:

  • p/x $rdx

输出

(unsigned long) $0 = 0x0123456789abcdef

然后输入

  • p/x $dx

这将打印出DX寄存器,该寄存器是EDX寄存器的最低有效部分。 因此,这是一个半字。 您应该看到以下内容:

0xcdef

再输入

  • p/x $dl

这将打印出DL寄存器,它是DX寄存器的最低有效位-这次是一个字节。 您应该看到以下内容

0xef

最后输出如下指令

  • p/x $dh

这为您提供了DX寄存器的最高有效部分,即DL给出的另一半。 DL中的L代表“低”而DH中的H代表“高”也就不足为奇了。
探索汇编时,请注意不同尺寸的寄存器。 寄存器的大小可以为其中包含的值提供线索。 例如,您可以轻松地查找通过AL寄存器返回布尔值的函数,因为布尔值将使用8个字节,而AL是64位“返回值寄存器” RAX的8位部分

寄存器 R8 到 R15

由于R8至R15系列寄存器仅针对64位架构而创建,因此它们使用完全不同的格式表示较小的寄存器。

现在,您将看下R9的不同大小如何选择。生成并运行Registers应用程序,然后暂停调试器。像以前一样,将相同的十六进制值写入R9寄存器:

  • register write $r9 0x0123456789abcdef

输入以下内容,以确认您已设置R9寄存器成功:

  • p/x $ r9

输入以下内容,这将打印R9寄存器的低32位。请注意,它与您为RDX指定低32位(即EDX)的方式有何不同。

  • p/x $ r9d

然后输入以下内容,这次您获得R9的低16位。同样,这与您为RDX进行此操作的方式不同

  • p/x $ r9w

再输入以下内容,打印出R9的低8位

  • p/x $ r9l

尽管这看起来有些乏味,但是您正在建立阅读汇编语言的技巧。

内存中断

现在,您已经了解了指令指针,是时候进一步探索其背后的内存了。顾名思义,指令指针实际上是一个指针。它不执行RIP寄存器中存储的指令,而是执行RIP寄存器中指向的指令。

在LLDB中看到这一点也许会更好地描述它。返回Registers应用程序中,打开AppDelegate.swift并再次在aBadMethod上设置一个断点。生成并运行该应用程序。

命中断点并停止程序后,导航回到汇编视图。如果您忘记了该操作,但尚未为其创建键盘快捷键,则可以在Debug \ Debug Workflow \ Always Show Disassembly下找到它。
您会看到一堆汇编指令。看一下RIP寄存器的位置,该位置应指向函数的最开始。
对于该项目,aBadMethod的起始地址始于0x100008910。和往常一样,您的地址可能会有所不同。
在LLDB控制台中,键入以下内容:

  • cpx $rip

到现在为止,这将打印出指令指针寄存器的内容。如预期的那样,您将获得aBadMethod起始地址。 但是同样,RIP寄存器指向内存中的值。 它指的是什么? 嗯,您可以摆脱疯狂的C编码技巧(您还记得吗?)并取消引用指针,但是使用LLDB可以找到一种更为优雅的方法。
输入以下内容,将地址替换为您的aBadMethod函数的地址:

  • memory read -fi -c1 0x100008910

哇,该命令到底能做什么? memory read 采用一个值,并读取您提供的内存地址所指向的内容。 -f 命令是一个格式参数。 在这种情况下,它是汇编指令格式。 最后,您说的是只希望使用count或-c参数打印一条汇编指令。
您将获得类似于以下内容的输出:

->  0x1000017c0: 55  pushq  %rbp

这是一些很好的输出。 它告诉您十六进制(0x55)中提供的汇编指令以及操作码,这些指令是 pushq %rbp 操作

注意:等等,您看到’%’吗?! LLDB中存在一个错误,当您以指令格式打印代码时,该错误不符合您的汇编风格。 请记住,如果您看到这种情况,则源和目标操作数将被反转!这就是inter指令集和AT&T指令集的区别。

让我们再看一下输出中的 “ 55”。 这是整个指令(即整个pushq %rbp)的编码。 不相信我吗? 您可以验证它。 在LLDB中输入以下内容:

  • expression -f i -l objc – 0x55

这实际上要求LLDB将0x55解释为x64操作码。 您将获得以下输出:

$1 = 55 pushq%rbp

该命令有点长,但这是因为如果您在Swift调试上下文中,则需要切换到Objective-C上下文。 但是,如果移至Objective-C调试上下文,则可以使用简短得多的便捷表达式。

尝试单击Xcode左侧面板中的其他框架,以进入一个不包含Swift或Objective-C / Swift桥接代码的Objective-C上下文。 单击Objective-C函数中的任何框架。

下一步,在LLDB控制台中键入以下内容:

  • p/i 0x55

好多了,对吧?
现在,回到手中的应用程序。 在LLDB中键入以下内容,再次用aBadMethod函数地址替换地址:

  • memory read -fi -c10 0x1000017c0

你讲获得以下输出:

-> 0x100008910: 55                    pushq  %rbp
0x100008911: 48 89 e5              movq   %rsp, %rbp
0x100008914: 48 81 ec c0 00 00 00  subq   $0xc0, %rsp
0x10000891b: 4c 89 6d f8           movq   %r13, -0x8(%rbp)
0x10000891f: b8 01 00 00 00        movl   $0x1, %eax
0x100008924: 89 c7                 movl   %eax, %edi
0x100008926: e8 d5 05 00 00        callq  0x100008f00               ; symbol stub for: generic specialization <preserving fragile attribute, Any> of Swift._allocateUninitializedArray<A>(Builtin.Word) -> (Swift.Array<A>, Builtin.RawPointer)
0x10000892b: 48 89 c7              movq   %rax, %rdi
0x10000892e: 48 89 45 a8           movq   %rax, -0x58(%rbp)
0x100008932: 48 89 55 a0           movq   %rdx, -0x60(%rbp)

这里需要注意一些有趣的事情:汇编指令的长度可以变化。 看一下第一条指令,然后看输出中的其余指令。 第一条指令的长度为1个字节,用0x55表示。 以下指令的长度为三个字节。
确保您仍在Objective-C上下文中,并尝试打印出负责此指令的操作码。 它只有三个字节,所以您只需将它们连接在一起

  • p/i 0x4889e5

您将获得与 mov %rsp,%rbp 指令完全无关的另一条指令! 您会看到以下内容:

e5 89  inl    $0x89, %eax

是怎么做到的呢? 也许现在是谈论字节序的好时机。

字节序…这东西倒过来了

x64以及ARM系列体系结构设备均使用低位字节序,这意味着数据以最低有效字节在先的形式存储在内存中。 如果要将数字0xabcd存储在内存中,则会先存储0xcd字节,然后再存储0xab字节。
回到指令示例,这意味着指令0x4889e5将以0xe5、0x89、0x48的形式存储在存储器中。
返回到您先前遇到的mov指令,请尝试反转用于构成汇编指令的字节。 在LLDB中输入以下内容:

  • p/i 0xe58948

您现在将获得预期的汇编表示:

$2 = 48 89 e5  movq   %rsp, %rbp

让我们再看看一些小端实践的例子。 在LLDB中输入以下内容:

  • memory read -s1 -c20 -fx 0x100008910

此命令读取地址为0x100008910的内存。 -s1选项可读取1个字节的大小块,-c20选项可读取20个字节的大小块。 您会看到类似这样的内容:

0x100008910: 0x55 0x48 0x89 0xe5 0x48 0x81 0xec 0xc0
0x100008918: 0x00 0x00 0x00 0x4c 0x89 0x6d 0xf8 0xb8
0x100008920: 0x01 0x00 0x00 0x00

现在,将大小增加一倍,数量减少一半,就像这样:

  • memory read -s2 -c10 -fx 0x100008910

你会看到这样的输出

0x100008910: 0x4855 0xe589 0x8148 0xc0ec 0x0000 0x4c00 0x6d89 0xb8f8
0x100008920: 0x0001 0x0000

请注意,当将内存值分组在一起时,由于使用了低位字节序,它们将被颠倒。
现在,将大小再增加一倍,数量再减少一半:

  • memory read -s4 -c5 -fx 0x100008910

然后你会看到如下输出

0x100008910: 0xe5894855 0xc0ec8148 0x4c000000 0xb8f86d89
0x100008920: 0x00000001

与之前的输出相比,这些值再次被颠倒。
记住这一点非常重要,但在探索你自己的记忆时,这会让你变得很混乱。不仅内存的大小会给您一个潜在的错误答案,而且顺序也会给您一个潜在的错误答案。当你开始对着电脑大喊大叫时,当你试图弄清楚某样东西应该如何工作时,请记住这一点!

0%