程序的机器级表示 (5)

3.10 综合: 理解指针

汇编学到这里指针其实已经没什么好说的了, 主要下面几点需要提一下 :

1. 虽然指针有类型和数值两部分, 但是指针的类型并不是机器代码的一部分, 这只是C语言提供的一种抽象而已, 用来帮助程序员避免寻址错误.

2. 展开第一点来讲, 它为什么能帮助程序员避免寻址错误呢? 很简单, 一个int类型的指针 int *iPtr 和一个char类型的指针 int *cPtr 都加上1, 实际来讲iPtr加了4, 而cPtr只加了1, 但是这并不需要程序员实际去考虑, 而是在执行运算时已经被隐式执行了, 这就是所谓的指针运算需要用对象大小对偏移量进行伸缩.

3. 对于函数指针, 应该是如下声明格式 : int (*fp) (int, int*), 读取的方法应该从fp开始向外读... (这一部分不详细讲, 我觉得这块讲得好的反而是C++ primer, 感兴趣的可以去看看, 我记得在前几章就讲到了)

3.11 应用 : 使用GDB调试器

这部分我不太想讲, 我平时gdb几乎就不用, 而且书上也只是带过而已, 想要了解这一块还需要专门去学习...

3.12 存储器的越界引用和缓冲区溢出

从前面的学习中我们已经发现了, C语言对于数组引用并不进行边界检查, 所以局部信息和状态信息(保存的寄存器的值和返回地址等等)都在栈中, 如果出现越界数组对于非法区域的读写, 就可能会破坏栈帧的状态信息, 如果之后重新恢复寄存器信息或者执行ret的话, 这时候就有可能出现严重的错误...

我们先来看看缓冲区溢出 :

对应的汇编代码是这样的 :

那么首先要明确数组是从低地址开始向高地址读的 :

另一方面, 缓冲区溢出甚至可以使得程序执行它并不愿意的函数, 这就是很常见的通过网络攻击系统安全的方法. 这边将系统攻击的我就不多赘述, 就简单说说, 这边也主要是作为背景知识了解, 开阔视野而已, 可以自己看书. 常用的对抗缓冲区溢出攻击的三种策略 :

1.  栈随机化 : 因为在早期的系统中, 程序栈地址非常容易预测(程序的栈帧位置相对固定), 这使得攻击者可以设计同一个程序对多个相似的机器进行攻击, 也就是所谓的安全单一化. 对抗的办法叫栈随机化, 也就是在程序开始之前, 在栈上随机分配一段0~n字节的随机大小的空间, 这样就导致了栈地址的随机波动, 但是这个也有缺点 : 过小的话, 攻击者可以使用遍历的方式来猜测出实际的程序地址, 过大的话浪费内存空间.

2. 栈破坏检测: 就是stack-protecter, 这个机制的核心在于在栈状态(过程刚开始保存寄存器和返回地址的那一部分内存)与局部缓冲区(实际保存局部变量的那一部分)直接插入一个canary value, 如果这个值被改写了, 每次返回时检查这个值是否被改写, 如果被改写那么程序异常退出... 如下图你可以看到第6, 7行就是对于canary value的保存, 15~17行就是这个越界检测.

3. 限制可执行代码区域 : 系统访问形式有三种(可读, 可写, 可执行), 过去的x86体系结构通常将读和执行合并为1位的标志, 这样的话栈上必须是可读可写, 所以也是可执行的. 另一方面一个典型的程序, 只有保存编译器产生的代码的那部分存储器才应该是可执行的, 其他部分应该只允许读或者写, 这就与之前产生了矛盾, 过去的x86使用了需要多的机制来是的某些位置只可读而不可执行, 但是带来了严重的性能损失. 后来AMD为他的64位处理器引入了NX(no- execute)位, 分离了读和执行, 并且这是由硬件来进行检查的, 使得栈可以被标记为可读, 可写但是不可执行, 同时不带来性能损失.

3.13 x86-64 : 将IA32 扩展到64位

首先来看下与IA32的区别和联系:

1.  数据类型 :

2. 汇编代码示例 :

对于上面总结的现象而言 : 通常来讲, x86-64的代码更加简洁, 需要更少的存储器访问, 运行起来相对IA32更有效率...

3.13.3 访问信息

先来看看x86-64的通用寄存器 :

总结下来我感觉区别有下面几点 :

1. 通用寄存器数量 : 8 --> 16

2. 通用寄存器大小 : 32 --> 64

3. 可直接访问每个寄存器的低8位(之前只有前四个可以) 和低32位...

4. %rsp延续了%esp的作用 : 保存指向栈顶的指针, 但是%rbp不再用来保存帧指针, 而变成了通用寄存器.

另一方面, 为了弥补IA32中偏移量(Imm(E, E, s)中的imm)只有32位长度的情况(现在的寄存器是64位长, 所以imm只有32位可能不太够), x86-64还加入了一些PC-relative操作数寻址方式(在IA32中只有跳转和其他控制跳转指令才能使用, 其实也就是指令中并不保存跳转位置的实际地址, 而是相对于当前的program counter的值计算位置差), 现在这种方式可以显示地用来寻址... 在x86-64中这个值存放在%rip(程序计数器)当中, 下面是一个例子 :

可以看到汇编代码的第5行, 为了读取存放在存储器中的gval2的值, 使用了这种PC相对数据寻址... 这是IA32中所不支持的...

下面是一些x86-64可用而IA32中没有的数据传送指令 :

上表中有几点要注意 :

1. 指令中R表示寄存器, D表示寄存器或者存储器, I表示立即数, S表示寄存器, 存储器或者立即数.

2. 对于movq而言, 如果源数值为32位立即数, 那么它将被符号扩展为64位...

3. 对于x86-64而言, 传送或者产生32位寄存器值的指令(movl), 高32位也会设置为0, 同理, movzbq, movzbl的高位都会设置为0而类似movb, movw的指令则不改变高位的值.

习题3.47里面要注意的是从long转变成int时, 使用movl(高32位自动被设置为0,相当于零拓展)或者使用movslq(相当于符号拓展)都可以, 因为后续操作中高32位都会被忽略...

算术指令

对于之前的算术指令, 在从IA32拓展到x86-64之后都进行了相应的扩展, 例如addq, incq, salq等等... 但是这里还有一点要重申一下, 对于只要涉及到传送或者产生32位的结果的指令, 高32位也会设置为0, 这一点不同于16位, 8位指令... movq和movabsq的区别在于, 前者如果源操作数是立即数, 最多是32位, 而movabsq中, 源操作数必须是64位立即数. 所以你会发现当拓展到64位之后, 出现了很多细节问题. 下面就是一个例子 :

可以看到处理符号拓展既可以使用cltq也可以使用movslq, 效果相同...

这里要先注意一下cltq, 可以回忆一下IA32中的cltd, 两者都是符号拓展到64位, 但是cltd将高32位符号拓展至%edx, 而cltq将高32位符号拓展至%rax的高32位中(两者低32位均在%eax当中), 所以可以看出其实cltd更应该对于这里的cqto... 

原文地址:https://www.cnblogs.com/nzhl/p/5660821.html