C指针:避不开的话题
指针,表征内存地址的一个变量。
与其他编程语言比较,由于指针使用的灵活性和高效性,常常和C的数组/结构体一同使用,算是C语言的一个重要特点了。
一个和指针类型有关的陷阱
笔者在从stack中取数时,发现这样一个问题:
栈底的位置位于0xFFC2_0000,栈顶的位置位于0xFFC0_0000,假设栈底48个字节的数据如下排放:
address | data |
---|---|
0xFFC2_0000 | 0x00 |
0xFFC1_FFFF | 0xFF |
0xFFC1_FFFE | 0xFE |
0xFFC1_FFFD | 0xFD |
… | … |
0xFFC1_FFF5 | 0xF5 |
0xFFC1_FFF4 | 0xF4 |
… | … |
0xFFC1_FFF0 | 0xF0 |
… | … |
0xFFC1_FFE0 | 0xE0 |
… | … |
0xFFC1_FFD0 | 0xD0 |
指针pBStack指向0xFFC2_0000,如果pBStack为UINT32类型,即无符号4字节整型,那么通过减法运算,永远无法取出以0xFFC1_FFF5为起始地址的32位数据,即用以下code永远无法访问到0xF8F7F6F5的数据:
pBStack = (UINT8*)(UINTN)STACK_BOTTOM;
for(index = 0; index < 48; index++){
printf("Addr = 0x%x, Data = 0x%x\n", pBStack-index,
*((volatile UINT32*)(pBStack-index)));
}
回溯指针的算数运算
指针加上或减去一个整型数据,还是一个指针,问题在于这个指针究竟指向哪个数据单元呢?很显然,下面这些运算结果都是不一样的:
1.(UINT8 *)p + 1
2. (UINT16 *) p + 1
3. (UINT32 *) p + 1
4. (UINT64 *) p + 1
因为当一个指针和一个整数进行加减运算的时候,整数会在执行运算前根据指针的类型进行一定的调整;因此上述1的结果指向下一个字节,2的结果指向+2个字节处,3的结果指向+4个字节处,4的结构指向+8个字节处。
以C语言中常见的数据类型为例,下面列出指针运算的实际指向结果:
还有一种算数运算,即两个指针之间做减法的运算,其结果也是会根据指针的类型进行调整,计算结果并不代表两个地址之间的字节数,而是单元个数。假设存在两个指针p和q,下面这些计算结果也是不一样的:
指针类型 | p-q的结果 |
---|---|
UINT *8 | p和q之间地址的差值 |
UINT *16 | p和q之间地址的差值/2 |
UINT *32 | p和q之间地址的差值/4 |
UINT *64 | p和q之间地址的差值/8 |
那么这里更加清楚的看到,指针是指向数据单元的,并不能简单的说其指向一个数据/内容。数据单元的维度是这个指针关键的一个特性。当然还有一个容易理解,容易被初学者接受的方式:假设地址p和地址q分别指向数组里的第x+1个元素和第x个元素,而且p和q与数组的数据类型相同(数组a[]为float型,指针p和指针q也为float型),那么p-q的结果就是中间相隔的元素的个数,结果一定为1!
回溯指针的比较运算
学过汇编的朋友们都知道,比较运算cmp的本质是两个操作数a, b做减法,如果ZF/SF等标志,就可以确定a, b两个数谁大谁小;
因此,可以说指针的比较运算本质上是指针的减法运算,上述p-q的运算也分析过了,下面仅给出一个容易被忽略的指针和数组元素地址比较的case:
假设有数组UINT32 array[],其维度为3,使用UINT32 *pa对其元素进行索引:
#define NUM 3
UINT32 array[NUM];
UINT32 *pa;
那么下面这两种给数组元素赋值0的方式,其结果也是不同的:
//方式1
for(pa = &array[0]; pa < &array[NUM];)
*pa++ = 0;
//方式2
for(pa = &array[NUM]; pa >= &array[0];)
*--pa = 0;
各位可以看出方式1和方式2的差异在哪里吗?同样都是数组元素的遍历和赋值,区别在哪里呢?下面就根据执行过程和可能发生的问题做一个描述:
方式1从数组的第一个元素开始,清零,第二个元素,清零,第3个元素也相同,到pa自加第四遍的时候,进行指针的比较运算,会发现pa = &array[3],那么停止清零操作;
方式2从数组的最后一个元素(第3个元素)开始,清零,再第2个元素,第1个元素,到pa自减第4遍的时候,此时的pa已经小于array[0]的地址了,这个时候就有可能发生指针跑飞的现象!
方式2正确的改发是这样,让pa = &array[0]的时候就终止:
//方式2
for(pa = &array[NUM]; pa > &array[0];)
*--pa = 0;
再看开篇问题
明确了指针的算数运算和比较运算之后,再回过头来看看开篇提到的栈上数据访问的问题。
address | data |
---|---|
0xFFC2_0000 | 0x00 |
0xFFC1_FFFF | 0xFF |
0xFFC1_FFFE | 0xFE |
0xFFC1_FFFD | 0xFD |
… | … |
0xFFC1_FFF5 | 0xF5 |
0xFFC1_FFF4 | 0xF4 |
… | … |
0xFFC1_FFF0 | 0xF0 |
… | … |
0xFFC1_FFE0 | 0xE0 |
… | … |
0xFFC1_FFD0 | 0xD0 |
pBStack的指针如果是UINT32 *类型,从0xFFC2_0000 stack bottom开始,通过减法只能访问到0xFFC1_FFFC/0xFFC1_FFF8/0xFFC1_FFF4/0xFFC1_FFF0这样地址的数据,因为这里每4个字节组成一个数据单元,每一个指针指向这4个字节的数据单元,可以称之为32Bit对齐。
如果要想访问到0xFFC1_FFF5为起始地址的单元,又不该栈的栈底地址,那么pBStack的类型就得声明为UINT8 *类型;
如果想配合sizeof()对stack中的数据访问,那我们也是统一用UINT8 *类型,就可以轻松避免指针算数运算的陷阱了。