3.7.1 数据对齐
许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2、4或8)的倍数。这种对齐限制简化了形成处理器和内存系统之间接口的硬件设计。例如,假设一个处理器总是从内存中取8个字节,则地址必须为8的倍数。如果我们能保证将所有的double类型数据的地址对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。无论数据是否对齐,x86-64硬件都能正确工作。不过,Intel还是建议要对齐数据以提高内存系统的性能。对齐原则是任何K字节的基本对象的地址必须是K的倍数。可以看到这条原则会得到如下对齐:
确保每种数据类型都是按照指定方式来组织和分配,即每种类型的对象都满足它的对齐限制,就可保证实施对齐。编译器在汇编代码中放人命令,指明全局数据所需的对齐。例如,下面这样的命令:
.align 8
这就保证了它后面的数据(在此,是跳转表的开始)的起始地址是8的倍数。因为每个表项长8个字节,后面的元素都会遵守8字节对齐的限制。
对于包含结构的代码,编译器可能需要在字段的分配中插入间隙,以保证每个结构元素都满足它的对齐要求。而结构本身对它的起始地址也有一些对齐要求。
比如说,考虑下面的结构声明:
struct S1 {
int i;
char c; int j;
};
假设编译器用最小的9字节分配,画出图来是这样的:
它是不可能满足字段i(偏移为0)和j(偏移为5)的4字节对齐要求的。取而代之地,编译器在字段c和主之间插入一个3字节的间隙(在此用蓝色阴影表示):
结果,j的偏移量为8,而整个结构的大小为12字节。此外,编译器必须保证任何struct s1*类型的指针p都满足4字节对齐。用我们前面的符号,设指针p的值为x。那么,x必须是4的倍数。这就保证了 p->i(地址 x)和 p->j(地址x十8)都满足它们的4字节对齐要求。
另外,编译器结构的末尾可能需要一些填充,这样结构数组中的每个元素都会满足它的对齐要求。
3.7.2 内存越界引用
内存越界引用是指程序在访问数组时,超出了数组的实际边界。在C语言中,数组的引用通常不进行边界检查,这意味着程序员需要自己确保不会访问超出数组分配的内存范围。如果访问了超出数组边界的内存,可能会导致程序崩溃、数据损坏或其他不可预测的行为。
内存越界引用的原因和影响
原因:
(1)局部数据存储:在C语言中,局部数据(如数组、结构体)通常存储在栈上。如果访问了栈上的数组元素超出了其分配的内存范围,就会发生内存越界引用。
(2)指针操作不当:使用指针进行数组访问时,如果没有正确计算索引,很容易导致越界。
影响:
(1)数据损坏:越界访问可能会修改其他变量的值,导致程序行为异常。
(2)程序崩溃:访问未分配的内存可能导致程序崩溃或产生不可预测的行为。
(3)安全风险:在安全敏感的应用中,内存越界可能导致缓冲区溢出攻击,从而执行恶意代码。
调试和预防内存越界引用的方法
调试方法:
(1)断点调试:在可能发生越界的代码处设置断点,逐步执行并观察变量的变化。
(2)内存检查工具:使用工具如Valgrind等来检测内存越界和泄漏问题。
预防措施:
(1)边界检查:在访问数组前,检查索引是否在合法范围内。
(2)使用安全的函数:避免使用可能导致缓冲区溢出的函数,如strcpy
、sprintf
等,改用安全的替代函数如strncpy
、snprintf
等。
(3)代码审查:定期审查代码,确保没有潜在的内存越界问题。
3.7.3 缓冲区溢出和缓冲区溢出攻击
1.原理
缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想的情况是,程序会检查数据长度,并且不允许输入超过缓冲区长度的字符。但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区,又被称为“堆栈”,在各个操作进程之间,指令会被临时储存在“堆栈”当中,“堆栈”也会出现缓冲区溢出。
2.攻击方式
缓冲区溢出攻击是利用缓冲区溢出漏洞所进行的攻击行动。攻击者可以通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。如果该程序有root权限,攻击者就获得了一个有root权限的shell,可以对系统进行任意操作。缓冲区溢出攻击可以导致程序运行失败、系统关机、重新启动等后果,甚至可以利用它执行非授权指令,取得系统特权,进而进行各种非法操作。
3.检测和预防
检测和预防缓冲区溢出是一个重要的安全问题。由于缓冲区溢出漏洞在C、C++等不提供内存越界检测功能的语言中编写的程序中广泛存在,因此,对于这类程序,需要特别注意检测和预防缓冲区溢出。具体的检测和预防方法可能包括:
- 代码审查:对程序代码进行严格的审查,确保数据长度的检查和执行权限的控制。
- 使用安全函数:在编程时使用提供边界检查的安全函数,避免使用不安全的函数。
- 工具检测:使用专门的工具对程序进行检测,发现潜在的缓冲区溢出漏洞。
- 权限控制:限制程序的执行权限,防止攻击者利用缓冲区溢出获得更高的权限。