![e60455a9be97687245e2f2ab761582c4.png](https://i-blog.csdnimg.cn/blog_migrate/8c35a2a9bbdd441d338e37a43af0cc59.jpeg)
我们知道结构体在栈上是连续的内存,结构体的指针则是执行这块内存的最低位。但是如果我们以结构体的成员大小来手动做地址偏移,可能会得到不符合预期的行为。这就是因为内存对齐的原因,所以结构体的字段并非是紧密填充的。
3.10 对齐
对齐是一种存储器读取优化。举个例子,由于总线宽度的原因,处理器可能每次取数据都是取4字节的数据,则取数据时地址需要为4的倍数,如果一个四字节数据,地址不是4的倍数,则必然需要取两次数据才能获取。
Linux上short需要2字节对齐。int,float,double等需要4字节对齐。 Windows上要求任意K字节的数据地址都必须是k的倍数。其他double必须为8字节对齐。
例如:
struct s1 {
int i;
char c;
int j;
};
上面结构体的内存结构,我们以为可能是1,实际上是2
![ce78c3c7f514e1762a697e12b3076c1a.png](https://i-blog.csdnimg.cn/blog_migrate/0cb1a23f2b51a9e11eceb59d9f900b8e.png)
3.11 理解指针
指针跟汇编里面的地址等同,所以非常的底层,非常的高效。
本节提的都是指针的基本知识,跳过不表。
3.12 使用GDB调试
跳过
3.13存储器的越界引用和缓冲区溢出
前面我们也尝试分析过了,通过栈地址可以访问一下不属于当前函数权限的栈内容,随意修改可能会引起无法预料的错误。
C语言对数组引用不进行边界检查,而溢出的方向是地址增大的方向,我们知道地址增大是靠近一些栈恢复的关键信息和返回函数的信息的。如果遭到破坏,会出现非常严重的错误。
通过精心设计的缓冲区溢出,就可以让程序执行本不改执行的代码。例如把栈中ret返回的地址给修改了,则当前函数返回之后,则会跳到新的代码处,这就是缓冲区溢出攻击。
大名鼎鼎的蠕虫病毒就是通过对fingerd程序实施缓冲区攻击以获取计算机权限。
缓冲区溢出偶尔也能被自己利用。文中提到了一个案例,微软的IM,兼容了AOL的聊天系统,后来AOL为了屏蔽微软的IM,利用自己的客户端的一个缓冲区溢出bug来检测是否是自己的客户端。
bug利用好了,就是功能 :)
3.15 在C中嵌入汇编代码
这是个大杀器,但是自己手写的汇编有的时候可能还不如优化编译器生成的代码呢。除非是一些特殊目的。
现代编译器基本上使得性能优化不再是使用汇编代码的原因了。高质量的编译器产生的代码甚至比手工编写的更好。但是也有一些必须要使用汇编的情况:
- 操作系统级别的代码,需要访问一些特殊寄存器等
- 应用程序想要访问条件码等机器特性
3.15.1 级别的内嵌汇编
基本的格式如asm(code-str)
,编译器会将这个code-str直接插入到产生的汇编代码中。
示例,例如一个想要访问条件码的函数如下:
// 如果x*y溢出则返回0,否则返回1 加法溢出可以通过ret < x 来判断,乘法还没有提过有什么直接的方法,
// 这里试图访问CF标志
int ok_mul(int x, int y, int *dest) {
int result = 0;
*dest = x * y;
asm("setae %al");
return result;
}
我们知道%eax是用来存储返回值的,所以在ret前面设置eax的低位值来得到结果。看起来似乎可行,但实际上在GCC中上述代码总是返回0 。因为GCC有它自己的想法,不以你的意志为转移 : ) 。它会在最后设置result = 0,在我们的asm语句之后,所以也就等于没执行asm语句。
3.15.2 asm的扩展格式
由于上述问题,编译器无法理解程序员到底要怎么使用寄存器,于是GCC提供了一个扩展版本,允许代码指定寄存器的一些信息。格式如:
asm (code-str [:output-list [: input-list [ : overwrite-list] ] ])
以上面那个函数为例:
int ok_mul(int x, int y, int *dest) {
int result = 0;
*dest = x * y;
asm("setea %%bl; movzbl %%bl,%0"
: "=r" (result) /* output */
: /* no inputs */
: "%ebx" /* overwrites */
)
return result;
}
output和input中列出的操作数由%0到%9
来引用。第四行的%0即output中指定的result要使用的寄存器。
"=r" (result)
中的"=r"
表示赋值,整数寄存器
, (result)
则是表示要指向的代码中可赋值的变量。
对于unsiged,GCC默认也使用有符号乘法,则进位标志可能不对,这里可以手动使用无符号乘法版本:
int ok_mul(unsigned x, unsigned y, unsigned *dest) {
int result = 0;
asm("movl %2,%%eax; mull %3; movl %%eax,%0; setea %%dl; movzbl %%dl,%1"
: "=r" (*dest), "=r" (result) /* output */
: "r" (x), "r" (y) /* inputs */
: "%eax", "%edx" /* overwrites */
)
return result;
}
对于overwrite申明的寄存器,函数的代码其他部分,则不会再使用。
需要注意的是,这样的汇编代码可能只在GCC下可以运行,移植性将大大降低。
在尝试汇编嵌入的时候,可以使用gcc -S指令产生成的代码是否符合预期,以便调整。
总结
通过本章的学习,我们大致了解了以下内容:
- GCC通过-S参数可以产生汇编中间代码,使我们学习汇编的一个好方法
- GCC汇编代码中的一些常用指令,例如
mov add sub lea push pop
。指令常常会附加一些后缀,如l b w
等,所以实际看到的指令可能是movl addl subl
等。 - 汇编代码的寻址方式
Imm(E,B,S)
,对于mov add
等指令,它是计算地址,并获取相应地址上的内存数据。而lea则是只计算地址,也就是不会内存中查找数据。这个特性可以用来计算寄存器中数值,来代替一些add即乘法。 - 常见的代码结构
if-else while for
等在汇编中大概是个什么结构。代码中少用goto来跳转,让汇编自己跳去吧。 - 条件码:CF, ZF, SF, OF ,平时其实也用不着。
- switch在分支足够多的情况下使用跳转表,从而才会比if快。
- 帧栈是一对指针,使用
%ebp, %esp
来迭代位移。%ebp
在函数过程中不动,常用来取参数,%esp
在压栈出栈的时候会变化。栈地址是往下增长的,即%esp的地址比%ebp要小。 - 函数的参数入栈的顺序和参数顺序是反的,即第一个参数是最后入栈。
- call指令自动将下一条指令的地址入栈,以便返回,再进行跳转。可被缓冲区溢出攻击,以跳转到非法代码。
经过了本章的学习,对C语言等高级语言的背后有了一点新的认识。对于一些行为准则的根本原因也有所了解,例如不要返回局部变量的指针
背后的实际运转情况等等。想必在今后的编程生涯中,可能会有一些奇妙的反应。