绪论
指针就是变量的内存地址,是C语言中最复杂的概念,为什么它如此容易出问题?因为,之前的操作都是编译器替我们算好内存地址,用汇编去操作内存。而用指针,需要程序员直接去操作变量的地址,没有中间编译器做计算和检查了,代码一多人脑捋不清楚,一不小心就会搞错,出现各种操作指针的bug【野指针、空指针……】
指针有基本数据类型指针、数组指针、多维数组指针、结构体指针、函数指针、指针函数、指针传参,数组传参等等,太多了!为了介绍的清楚些,把指针分层若干个章节进行介绍,本章就介绍基本数据类型的指针和汇编程序之间的关系!
正文
老规矩,先来一段简单的c代码:
#include<stdio.h>
int main(){
int i = 15;
int *p = &i;
int j = *p;
return 0;
}
下面是它对应的汇编程序代码:
subq $32, %rsp # 将 rsp 寄存器减去32,给main函数分配栈帧空间
movq %fs:40, %rax # 将 fs 段寄存器中偏移量为 40 的值复制到 rax 寄存器中
movq %rax, -8(%rbp) # 将用于安全检查的栈保护数据,放到main函数的栈帧里备份,因为后面做安全检查时要用到
xorl %eax, %eax # 自己与自己异或,将自己清零
## int i = 15;
movl $15, -24(%rbp) # 给int i 分配内存,rbp - 24指向的内存处,放入int数据15
## int *p = &i;
leaq -24(%rbp), %rax # 计算rbp-24的值,并将该值放到rax,该值其实就是变量int i = 15的内存地址
movq %rax, -16(%rbp) # 给指针变量p分配内存,将变量i的内存地址,放到rbp-16内存里
## int j = *p;
movq -16(%rbp), %rax # 将rbp-16指向的内存数据,放到rax寄存器里,实际就是变量i的内存地址
movl (%rax), %eax # rax存的是变量i的地址,(%rax)是取出该内存地址里的数据,然后放到了eax寄存器里
movl %eax, -20(%rbp) # 给int j分配内存,将刚才拿出来的数据15放到rbp-20里, 所谓的 *p 对应的汇编就是 (%rax),因为()是取内存数据的意思
movl $0, %eax
movq -8(%rbp), %rdx
xorq %fs:40, %rdx # 取出刚才备份的栈安全检查数据,若异或后是0,说明安全检查数据没变化是原值,下面je指令发现上面的执行结果是0,就跳转,否则执行结果不是0,说明2次栈安全检查数据不一样,就不跳转
je .L3
call __stack_chk_fail@PLT # 没跳转,则执行到这里,报错,终止程序运行【这条指令调用了__stack_chk_fail函数来检测栈溢出攻击,并使用了过程链接表来实现动态链】
.L3:
leave
.cfi_def_cfa 7, 8
ret
## 注意:
栈安全检查数据(stack canary)是一种用于防止栈溢出攻击的安全机制。它在函数调用时在栈上插入一个特殊的值(称为 canary),在函数返回前检查这个值是否被修改。如果被修改,说明栈溢出发生了,程序会终止运行。
栈溢出攻击通常是通过向栈上的缓冲区写入超过其分配大小的数据来实现的。这样会覆盖栈上的其他数据,包括返回地址和其他重要数据。攻击者可以利用这种方法来控制程序的执行流程。
栈安全检查数据可以防止这种攻击,因为它会在返回地址之前被覆盖。如果检测到栈安全检查数据被修改,程序会立即终止运行,防止攻击者控制程序的执行流程。
可以看出,&i取i的地址主要是用到了leaq指令来算出变量i的地址,而*p取出指针p所指的内存块里的数据主要是用到了(%rax)来取内存里的数据。这么一分析发现基本数据类型的指针操作还是很简单的,无非是计算变量的地址,并从该内存地址处拿出数据。
从上面也可以看出,变量i的地址,本身并不特殊,它也是一种二进制数据,把它算出来后,也可以存到栈帧内存里【leaq -24(%rbp), movq %rax, -16(%rbp)】