根据ARM过程调用标准,程序的执行总是从main函数开始返回值通常保存在R0中,若返回值为64位的,用来保存返回值的是什么?

ARM 汇编基础:函数和流程分支汇编

上一篇中,我们学习了 ,本篇我们继续看一下函数对应的汇编,从而了解函数的本质。
我们知道,调用函数的时候,汇编代码会首先开辟一段栈空间,用于存放参数、局部变量以及对寄存器进行保护,那么这些是怎么完成的呢?

先来看一段函数的代码:

运行工程,查看生成的汇编代码: 栈平衡(也就是回收函数所开辟的栈空间,这也就是我们在函数中声明的局部变量,出了这个函数就无法使用的原因)

上面的汇编代码,重要的都加了注释进行说明。从汇编代码可以清晰的看到,参数分别存储在 w0 和 w1 寄存器。

【函数 sum】的返回值存储在 w0 寄存器。

实际上,我们可以通过自己的代码,来验证一下:

叶子函数:函数中不再调用其他函数,就称为叶子函数,叶子函数不需要开辟栈空间。

经过上面的代码,我们总结一下:

  • 函数的返回值,通常都放在 x0寄存器 中。
  • 函数的参数,存放在 x0-x7 (w0-w7) 这 8 个寄存器里面。如果超过 8 个参数,则会入栈。

小示例:超过 8 个参数的函数

从汇编代码可以看到,【90】这个参数先放在了 w8 寄存器,然后入栈了:

在【函数 sumB】中求和的时候,是从栈中将参数【90】读取到 w1 寄存器,然后再相加的:

【思考】:在OC中,我们怎么知道调用的是哪个类的哪个方法?
在上面的代码演示过程中,我们可以看到,参数是保存在【x0】 -- 【x7】寄存器中的,OC 方法的调用,本质上是调用 objc_msgSend(id, SEL) 函数,第一个参数和第二个参数分别存放在 x0 和 x1 寄存器中,对应的就是具体的对象和具体的方法。因此我们查看 x0 和 x1 寄存器的值,就知道是哪个类的哪个方法了。

我们还是先上代码,从代码中来学习,比纯讲理论,要印象深刻:

运行工程,查看生成的汇编,下面的汇编代码,都添加了注释,方便大家理解:

从汇编代码可以看到,局部变量做了入栈操作,进行参数的保存。

上面的汇编代码,我们都是利用 Xcode 断点运行,开启【Debug】-【Debug Workflow】-【Always Show Disassembly】,通过 Xcode 调试中的信息查看到的。实际上,我们可以利用 IDA(神器,不解释)或者 Hopper,直接查看汇编代码。
打开 Xcode,新建一个示例工程,编写代码:

Xcode 直接编译工程,拷贝出生成的 .app 文件,显示包内容,拿到可执行文件,然后使用 IDA/Hopper 打开,如下图所示:
实际上,IDA/Hopper 会在汇编代码中,添加一些辅助的变量和其他内容,来更好的帮助我们还原和理解源码。我们来详细看一下示例代码的汇编,并尝试着进行高级代码的还原,过程直接通过注释解说了:

int g; ;这个是后面分析出来的,但值是无法得到的,得运行时分析 //参数类型是看调用的地方,传递的参数是什么 ;这两句代码是读取参数 ;这两句是读取全局变量到 x30,但类型暂时无法确定,类型得在后面分析 ;读取全局变量到 w1 ;后面三句代码是恢复x29, x30,并平衡栈空间

根据上面的逐句分析,然后优化整理:

可以看到和之前的代码基本一致。

第 3 小节中我们讲解了通过工具分析汇编,下面我们就直接用工具来进行汇编的讲解了。

62f8 cmp w0, w1 ;比较两个参数(cmp相当于做了一个减法 w0 - w1,结果不会影响 w0、w1,结果会影响 cpsr寄存器。cmp结果有三种:大于、等于、小于) ;得到全局变量的地址 并放入 x8 630c str w9, x8 ;w9寄存器中的值,放入 x8 所指向内存地址(也就是全局变量的地址) ;得到全局变量的地址 并放入 x8 str w9, x8 ;w9寄存器中的值,放入 x8 所指向内存地址(也就是全局变量的地址)

上一篇中,我们学习了 ,本篇我们继续看一下函数对应的汇编,从而了解函数的本质。
我们知道,调用函数的时候,汇编代码会首先开辟一段栈空间,用于存放参数、局部变量以及对寄存器进行保护,那么这些是怎么完成的呢?

先来看一段函数的代码:

运行工程,查看生成的汇编代码: 栈平衡(也就是回收函数所开辟的栈空间,这也就是我们在函数中声明的局部变量,出了这个函数就无法使用的原因)

上面的汇编代码,重要的都加了注释进行说明。从汇编代码可以清晰的看到,参数分别存储在 w0 和 w1 寄存器。

【函数 sum】的返回值存储在 w0 寄存器。

实际上,我们可以通过自己的代码,来验证一下:

叶子函数:函数中不再调用其他函数,就称为叶子函数,叶子函数不需要开辟栈空间。

经过上面的代码,我们总结一下:

  • 函数的返回值,通常都放在 x0寄存器 中。
  • 函数的参数,存放在 x0-x7 (w0-w7) 这 8 个寄存器里面。如果超过 8 个参数,则会入栈。

小示例:超过 8 个参数的函数

从汇编代码可以看到,【90】这个参数先放在了 w8 寄存器,然后入栈了:

在【函数 sumB】中求和的时候,是从栈中将参数【90】读取到 w1 寄存器,然后再相加的:

【思考】:在OC中,我们怎么知道调用的是哪个类的哪个方法?
在上面的代码演示过程中,我们可以看到,参数是保存在【x0】 -- 【x7】寄存器中的,OC 方法的调用,本质上是调用 objc_msgSend(id, SEL) 函数,第一个参数和第二个参数分别存放在 x0 和 x1 寄存器中,对应的就是具体的对象和具体的方法。因此我们查看 x0 和 x1 寄存器的值,就知道是哪个类的哪个方法了。

我们还是先上代码,从代码中来学习,比纯讲理论,要印象深刻:

运行工程,查看生成的汇编,下面的汇编代码,都添加了注释,方便大家理解:

从汇编代码可以看到,局部变量做了入栈操作,进行参数的保存。

上面的汇编代码,我们都是利用 Xcode 断点运行,开启【Debug】-【Debug Workflow】-【Always Show Disassembly】,通过 Xcode 调试中的信息查看到的。实际上,我们可以利用 IDA(神器,不解释)或者 Hopper,直接查看汇编代码。
打开 Xcode,新建一个示例工程,编写代码:

Xcode 直接编译工程,拷贝出生成的 .app 文件,显示包内容,拿到可执行文件,然后使用 IDA/Hopper 打开,如下图所示:
实际上,IDA/Hopper 会在汇编代码中,添加一些辅助的变量和其他内容,来更好的帮助我们还原和理解源码。我们来详细看一下示例代码的汇编,并尝试着进行高级代码的还原,过程直接通过注释解说了:

int g; ;这个是后面分析出来的,但值是无法得到的,得运行时分析 //参数类型是看调用的地方,传递的参数是什么 ;这两句代码是读取参数 ;这两句是读取全局变量到 x30,但类型暂时无法确定,类型得在后面分析 ;读取全局变量到 w1 ;后面三句代码是恢复x29, x30,并平衡栈空间

根据上面的逐句分析,然后优化整理:

可以看到和之前的代码基本一致。

第 3 小节中我们讲解了通过工具分析汇编,下面我们就直接用工具来进行汇编的讲解了。

62f8 cmp w0, w1 ;比较两个参数(cmp相当于做了一个减法 w0 - w1,结果不会影响 w0、w1,结果会影响 cpsr寄存器。cmp结果有三种:大于、等于、小于) ;得到全局变量的地址 并放入 x8 630c str w9, x8 ;w9寄存器中的值,放入 x8 所指向内存地址(也就是全局变量的地址) ;得到全局变量的地址 并放入 x8 str w9, x8 ;w9寄存器中的值,放入 x8 所指向内存地址(也就是全局变量的地址)

上一篇中,我们学习了 ,本篇我们继续看一下函数对应的汇编,从而了解函数的本质。
我们知道,调用函数的时候,汇编代码会首先开辟一段栈空间,用于存放参数、局部变量以及对寄存器进行保护,那么这些是怎么完成的呢?

先来看一段函数的代码:

运行工程,查看生成的汇编代码: 栈平衡(也就是回收函数所开辟的栈空间,这也就是我们在函数中声明的局部变量,出了这个函数就无法使用的原因)

上面的汇编代码,重要的都加了注释进行说明。从汇编代码可以清晰的看到,参数分别存储在 w0 和 w1 寄存器。

【函数 sum】的返回值存储在 w0 寄存器。

实际上,我们可以通过自己的代码,来验证一下:

叶子函数:函数中不再调用其他函数,就称为叶子函数,叶子函数不需要开辟栈空间。

经过上面的代码,我们总结一下:

  • 函数的返回值,通常都放在 x0寄存器 中。
  • 函数的参数,存放在 x0-x7 (w0-w7) 这 8 个寄存器里面。如果超过 8 个参数,则会入栈。

小示例:超过 8 个参数的函数

从汇编代码可以看到,【90】这个参数先放在了 w8 寄存器,然后入栈了:

在【函数 sumB】中求和的时候,是从栈中将参数【90】读取到 w1 寄存器,然后再相加的:

【思考】:在OC中,我们怎么知道调用的是哪个类的哪个方法?
在上面的代码演示过程中,我们可以看到,参数是保存在【x0】 -- 【x7】寄存器中的,OC 方法的调用,本质上是调用 objc_msgSend(id, SEL) 函数,第一个参数和第二个参数分别存放在 x0 和 x1 寄存器中,对应的就是具体的对象和具体的方法。因此我们查看 x0 和 x1 寄存器的值,就知道是哪个类的哪个方法了。

我们还是先上代码,从代码中来学习,比纯讲理论,要印象深刻:

运行工程,查看生成的汇编,下面的汇编代码,都添加了注释,方便大家理解:

从汇编代码可以看到,局部变量做了入栈操作,进行参数的保存。

上面的汇编代码,我们都是利用 Xcode 断点运行,开启【Debug】-【Debug Workflow】-【Always Show Disassembly】,通过 Xcode 调试中的信息查看到的。实际上,我们可以利用 IDA(神器,不解释)或者 Hopper,直接查看汇编代码。
打开 Xcode,新建一个示例工程,编写代码:

Xcode 直接编译工程,拷贝出生成的 .app 文件,显示包内容,拿到可执行文件,然后使用 IDA/Hopper 打开,如下图所示:
实际上,IDA/Hopper 会在汇编代码中,添加一些辅助的变量和其他内容,来更好的帮助我们还原和理解源码。我们来详细看一下示例代码的汇编,并尝试着进行高级代码的还原,过程直接通过注释解说了:

int g; ;这个是后面分析出来的,但值是无法得到的,得运行时分析 //参数类型是看调用的地方,传递的参数是什么 ;这两句代码是读取参数 ;这两句是读取全局变量到 x30,但类型暂时无法确定,类型得在后面分析 ;读取全局变量到 w1 ;后面三句代码是恢复x29, x30,并平衡栈空间

根据上面的逐句分析,然后优化整理:

可以看到和之前的代码基本一致。

第 3 小节中我们讲解了通过工具分析汇编,下面我们就直接用工具来进行汇编的讲解了。

62f8 cmp w0, w1 ;比较两个参数(cmp相当于做了一个减法 w0 - w1,结果不会影响 w0、w1,结果会影响 cpsr寄存器。cmp结果有三种:大于、等于、小于) ;得到全局变量的地址 并放入 x8 630c str w9, x8 ;w9寄存器中的值,放入 x8 所指向内存地址(也就是全局变量的地址) ;得到全局变量的地址 并放入 x8 str w9, x8 ;w9寄存器中的值,放入 x8 所指向内存地址(也就是全局变量的地址)

剩余50%的内容订阅专栏后可查看

小专栏是一个专业人士的创作知识社区,在这里您可以看到各个领域最专业的专栏和观点。

}

C/C++ 函数调用方式与栈原理是 C/C++ 开发必须要掌握的基础知识,也是高级技术岗位面试中高频题。我真的真的真的建议无论是使用 C/C++ 的学生还是广大 C/C++ 开发者,都该掌握此回答中所介绍的知识。

如果你看不懂接下来第二部分在说什么,但是仍然希望有朝一日能掌握第二部分的内容,可以直接跳到第三部分《从哪里可以系统地学习到这些知识?》。

二、你一定要搞明白的 C/C++ 函数调用方式与栈原理

这篇回答试图讲明当一个 C/C++ 函数被调用时,一个栈帧(stack frame)是如何被建立,又是如何被销毁的。

这些细节跟操作系统平台及编译器的实现有关,下面的描述是针对运行在 Intel 奔腾芯片上 Linux 的 gcc 编译器而言。C/C++ 标准并没有描述实现的方式,所以,不同的编译器、处理器、操作系统都可能有自己的建立栈帧的方式。

2.1 一个典型的栈帧

图 1 是一个典型的栈帧,图中,栈顶在上,地址空间往下增长。 这是如下一个函数调用时的栈的内容:

并且,foo 有两个局部的 int 变量(4 个字节)。

在这个简化的场景中,main 调用 foo,而程序的控制仍在 foo 中。这里,main 是调用者(caller),foo 是被调用者(callee)。

ESP 寄存器被 foo 使用来指示栈顶,EBP 寄存器存储“基准指针”

从 main 传递到 foo 的参数以及 foo 本身的局部变量都可以以基准指针 EBP 为参考,加上偏移量找到。由于被调用者允许使用 EAX、ECX 和 EDX 寄存器,所以如果调用者希望保存这些寄存器的值,就必须在调用子函数之前显式地把它们保存在栈中。

另一方面,如果除了上面提到的几个寄存器,被调用者还想使用别的寄存器,比如 EBX、ESI 和 EDI,那么,被调用者就必须在栈中保存这些被额外使用的寄存器,并在调用返回前恢复它们。也就是说,如果被调用者只使用约定的 EAX、ECX 和 EDX 寄存器,它们由调用者负责保存并恢复,但如果被调用这还额外使用了别的寄存器,则必须由它们自己保存并回复这些寄存器的值。

传递给 foo 的参数被压到栈中,最后一个参数先进栈,所以第一个参数是位于栈顶的。

foo 中声明的局部变量以及函数执行过程中需要用到的一些临时变量也都存在栈中。 小于等于 4 个字节的返回值会被保存到 EAX 中,如果大于 4 字节,小于 8 字节,那么 EDX 也会被用来保存返回值。如果返回值占用的空间还要大,那么调用者会向被调用者传递一个额外的参数,这个额外的参数指向将要保存返回值的地址。用 C/C++ 语言来描述这个过程就是函数调用:

由于 x 的内容占用的空间过大被转化为:

注意,这仅仅在返回值占用大于 8 个字节时才发生。有的编译器不用 EDX 保存返回值,所以当返回值大于 4 个字节时,就用这种转换。 当然,并不是所有函数调用都直接赋值给一个变量,还可能是直接参与到某个表达式的计算中,例如:

或者作为另外一个函数的参数, 例如:

这些情况下,foo 的返回值会被保存在一个临时变量中参加后续的运算。

让我们一步步地看一下在 C/C++ 函数调用过程中,一个栈帧是如何建立及销毁的。

2.2 函数调用前调用者的动作

在我们的例子中,调用者是 main,它准备调用函数 foo。在函数调用前,main 正在用 ESP 和 EBP 寄存器指示它自己的栈帧。

首先,main 把 EAX、ECX 和 EDX 压栈。这是一个可选的步骤,如果这三个寄存器即将被用到,但当前存储的内容需要保存下来以备将来恢复,则执行此步骤。

接着,main 把传递给 foo 的参数一一进栈,最后的参数最先进栈。例如,假设我们的函数调用是:

相应的汇编语言指令是(这里 12、15 和 18 都是立即数):

当 call 指令执行的时候,EIP 指令指针寄存器的内容被压入栈中。因为 EIP 寄存器是指向 main 中的下一条指令,所以现在返回地址就在栈顶了。在 call 指令执行完之后,下一个执行周期将从名为 foo 的标记处开始。

图 2 展示了 call 指令完成后栈的内容。图 2 及后续图中的双虚线指示了函数调用前栈顶的位置。我们将会看到,当整个函数调用过程结束后,栈顶又回到了这个位置。

2.3 被调用者在函数调用后的动作

当函数 foo,也就是被调用者取得程序的控制权,它必须做 3 件事

  1. 建立它自己的栈帧,为局部变量分配空间(如果需要,保存 EBX、ESI 和 EDI 等寄存器的值)。

首先,foo 必须建立它自己的栈帧。EBP 寄存器现在正指向 main 的栈帧中的某个位置,这个值必须被保留,因此,EBP 进栈保存当前值;然后 ESP 的内容赋值给了 EBP,这使得函数的参数可以通过对 EBP 附加一个偏移量得到,而栈寄存器 ESP 便可以空出来做其他事情。如此一来,几乎所有的 C/C++ 函数调用都从如下两个指令开始:

此时的栈如图 3 所示,在这个场景中,第一个参数的地址是 EBP 加 8,因为 main 的 EBP 和返回地址各在栈中占了 4 个字节。

  1. 下一步,foo 必须为它的局部变量分配空间,同时,也必须为它可能用到的一些临时变量分配空间。

例如,foo 中的一些 C/C++ 语句可能包括复杂的表达式,其子表达式的中间值就必须得有地方存放,因为它们可能被下一个复杂表达式所复用。为说明方便,我们假设 foo 中有两个 int 类型(每个占 4 字节)的局部变量,另外还需要额外的 12 字节作为临时存储空间。简单地把栈指针减去 20 便是为这 20 个字节分配了空间:

现在,局部变量和临时存储空间都可以通过基准指针 EBP 加偏移量找到了。 最后,如果 foo 用到 EBX、ESI 和 EDI 寄存器,则必须使用栈来保存这些寄存器的当前值。因此,现在的栈如图 4 所示。

foo 的函数体现在就可以执行了。这其中也许有进栈、出栈的动作,栈指针 ESP 也会上下移动,但 EBP 是保持不变的。这意味着我们可以一直用 [EBP+8] 找到第一个参数,而不管在函数中有多少进出栈的动作。 函数 foo 的执行也许还会调用别的函数,甚至递归地调用 foo 本身。然而,只要 EBP 寄存器在这些子调用返回时被恢复,就可以继续用 EBP 加上偏移量的方式访问实际参数、局部变量和临时存储空间。

2.4 被调用者返回前的动作

在把程序控制权返还给调用者前,被调用者 foo 必须先把返回值保存在 EAX 寄存器中。

我们前面已经讨论过,当返回值占用多于 4 个或 8 个字节时,接收返回值的变量地址会作为一个额外的指针参数被传到函数中,而函数本身就不需要返回值了。这种情况下,被调用者直接通过内存拷贝把返回值直接拷贝到接收地址,从而省去了一次通过栈的中转拷贝。 其次,foo 必须恢复 EBX、ESI 和 EDI 寄存器的值。如果这些寄存器被修改,正如我们前面所说,我们会在 foo 执行开始时把它们的原始值压入栈中。如果 ESP 寄存器指向如图 4 所示的正确位置,寄存器的原始值就可以出栈并恢复。可见,在 foo 函数的执行过程中正确地跟踪 ESP 是多么的重要——也就是说,进栈和出栈操作的次数必须保持平衡。

这两步之后,我们不再需要 foo 的局部变量和临时存储空间了,我们可以通过下面的指令销毁当前栈帧:

其结果就是现在栈里的内容跟图 2 中所示的栈完全一样。现在可以执行返回指令了。从栈里弹出返回地址,赋值给 EIP 寄存器。栈如图 5 所示:

i386 指令集有一条 “leave” 指令,它与上面提到的 mov 和 pop 指令所做的动作完全相同。所以,C/C++ 函数通常以这样的指令结束:

2.5 调用者在返回后的动作

在程序控制权返回到调用者(也就是我们例子中的 main 函数)后,栈如图 5 所示。这时,传递给 foo 的参数通常已经不需要了。我们可以把这 3 个参数一起弹出栈,这可以通过把栈指针加 12(3 个 4 字节)实现:

如果在函数调用前,保存过 EAX、ECX 和 EDX 寄存器的值,调用者 main 函数现在可以把它们弹出以恢复它们当时的值。这个动作之后,栈顶就回到了开始整个函数调用过程前的位置,也就是图 5 中双虚线的位置。

这段代码反汇编后,代码是什么呢?

ebp 的值入栈,保存现场(调用现场,从 test 函数看,如红线所示,即保存的 0x12FF80 用于从 test 函数堆栈返回到 main 函数):

此时 ebp=0x12FF80 此时 ebp 就是“当前函数堆栈”的基址,以便访问堆栈中的信息;还有就是从当前函数栈顶返回到栈底:

函数使用的堆栈,默认 64 个字节,堆栈上就是 16 个横条(密集线部分)此时 esp=0x12FF40。 在上图中,上面密集线是 test 函数堆栈空间,下面是Main的堆栈空间(补充,其实这个就叫做 Stack Frame):

函数调用,转向 eip 。

即转向被调函数 test:

因为 Win32 汇编一般用 eax 返回结果 所以如果最终结果不是在 eax 里面的话,还要把它放到 eax。

注意,从被调函数返回时,是弹出 EBP 恢复堆栈到函数调用前的地址,弹出返回地址到 EIP 以继续执行程序。 从 test 函数返回,执行:

清栈,清除两个压栈的参数10 和 90,由调用者 main 负责。 (默认函数调用方式是 __cdecl,该函数调用方式由调用者负责恢复栈,调用者负责清理的只是入栈的参数,test 函数自己的堆栈空间自己返回时自己已经清除)

 push eax ;入栈,计算结果108入栈,即printf函数的参数之一入栈
 

EBP=0x12FFC0 尘归尘 土归土 一切都恢复最初的样子了。

  1. 运行过程中 0x12FF28 保存了指令地址 是怎么保存的?栈每个空间保存 4 个字节(粒度 4 字节) 例如下一个栈空间 0x12FF2C 保存参数 10,因此:

Little-Endian 认为其读的第一个字节为最小的那位上的数。

  1. char a[] = "abcde" 对局部字符数组变量(栈变量)赋值,是利用寄存器从全局数据内存区把字符串“abcde”拷贝到栈内存中的。

可以看出来是从右边开始入栈,所以是 5 4 3 2 1 入栈,

如果上面是指针算术,那这里就是地址算术,只是首地址 + 1个字节的 offset,即 ebp-13h 给指针。实际保存是这样的:

注意,是 int* 类型的,最后获得的是 00 00 00 02,由于使用的是 Little-Endian, 实际上逻辑数是 ,转换为十进制数就为 ,最后输出 。

三、从哪里可以系统地学习到这些知识?

不知道你看了上文是否觉得特别吃力,如果觉得吃力,我推荐几本书可以让你系统地掌握这些知识。

学习汇编不是一定要用汇编来写代码,就像我们学习 C/C++ 也不一定单纯为了面试和找工作。

对于 C/C++ 的同学来说,汇编是建议一定要掌握的,只有这样,你才能在书写 C++ 代码的时候,清楚地知道你的每一行C++代码背后对应着什么样的机器指令,if/for/while 等基本程序结构如何实现的,函数的返回值如何返回的,为什么整型变量的数学运算不是原子的,最终你知道如何书写代码才能做到效率最高。掌握了汇编,你可以明白,在 C++ 中,一个栈对象从构造到析构,其整个生命周期里,开发者的代码、编译器和操作系统分别做了什么。掌握了汇编,你可以理解函数调用是如何实现的,你可以理解函数的几种调用方法,为什么 printf 这样的函数其调用方式不能是 __stdcall,而必须是 __cdecl。掌握了汇编,你就能明白为什么一个类对象增加一个方法不会增加其实际占的内存空间。

汇编的书籍首推王爽老师的《汇编语言(第三版)》,这本书比较薄,读起来轻松舒适,可以作为汇编入门的书籍。

当然,如果你想系统且严谨地学习下汇编且用于对计算机系统原理的理解(这本书不仅仅介绍汇编知识,还有其他内容),我强烈推荐一本经典著作,学计算机的同学基本没有不知道这本书的——《深入理解计算机系统》,这本书英文叫《Computer Systems:A Programmer’s Perspective》(程序员视角下的计算机系统,所以又称为 CSAPP)。

2. 第二个基础知识是编译、链接与运行时体系知识

作为一个开发者,要清楚地知道我们写的 C/C++ 程序是如何通过预处理、编译与链接等步骤最终变成可执行的二进制文件,操作系统如何识别一个文件为可执行文件,一个可执行文件包含什么内容,执行时如何加载到进程的地址空间,程序的每一个变量和数据位于进程地址空间的什么位置,如何引用到。一个进程的地址空间有些什么内容,各段地址分布着什么内容,为什么读写空指针或者野指针会有内存问题。一个进程如何装在各个 so 或 dll 文件的,这些文件被加载到进程地址空间的什么位置,如何被执行,数据如何被交换。

这里强烈推荐下《程序员的自我修养》这本书,搞 C/C++ 开发,读了一百本 C/C++ 相关的书籍,不如好好读一下这本书。

当然,还有另外一本可用于实战的书《老码识途 从机器码到框架的系统观逆向修炼之路》,这本通过实践行动告诉你,你写的每一行 C/C++ 代码是如何与相应的机器码对应起来的。

此书以逆向反汇编为线索,自底向上,从探索者的角度,原生态地刻画了对系统机制的学习,以及相关问题的猜测、追踪和解决过程,展现了系统级思维方式的淬炼方法。该思维方式是架构师应具备的一种重要素质。此书内容涉及反汇编、底层调试、链接、加载、钩子、异常处理、测试驱动开发、对象模型和机制、线程类封装、跨平台技术、插件框架、设计模式、GUI框架设计等。

你可以跟着这本书的内容在调试器中一步步看你的 C/C++ 代码在系统层级是如何运行的。我第一次知道有此书时如获至宝,连夜下单购买。

当然,还有一本关于 C++ 反汇编的书叫《C++反汇编与逆向分析技术揭秘》,这本书也是非常不错的书,可以学习汇编和反汇编,学习 C++ 代码如何与编译后的机器码相对应。

这本书最后的案例是逆向分析大名鼎鼎的熊猫烧香病毒。

图书来源于网络,喜欢请购买正版,侵删。

原创不易,觉得有帮助,请点赞和关注 ~

}

我要回帖

更多关于 程序的执行总是从main函数开始 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信