在c99协议标准中,增加了变长数组(VLA)这一特性,本文旨在从汇编的角度来理解其原理,并且简单阐述下数组越界保护的内容。
在此顺带说一下自己对c语言学习的理解,关于常规的表达式、语法等不做阐述,主要对一些复杂的关键字或者特性的学习方式作以自己的感悟。如:c语言中对const、static、变长数组等的学习。
1.通过尝试式学习,即通过代码实现进行尝试说明。
2.通过的内存段工具binutils,可以通过工具查看分析代码的段、符号、汇编等。
3.通过汇编代码进行学习,汇编作为分析调用栈不可或缺的技能,当然需要我们予以重视。
4.通过地址进行学习,如果我们懂了地址与内存之间的关系,可以通过地址推断出一些c的特性原理,如两个变量地址间的关系等
利用以下代码进行说明,里面有定长和变长数组的代码,在此通过汇编进行说明,采用arm交叉编译链进行说明;
#include <stdio.h>
void array()
{
int a[10];
}
void array_vla(int x)
{
int a[x];
}
int main(int argc,char *argv[])
{
array();
array_vla(10);
return 0;
}
通过gcc编译后,使用objdump工具查看其汇编代码:
1.定长数组的使用
定长数组,应该都很熟悉了,是对同一种类型的描述,如int a[10];明显这是定义了长度为10的数组,类型为int类型。
定长数组使用函数array(),我们通过汇编进一步说明。
0001040c <array>:
1040c: e52db004 push {fp} ; (str fp, [sp, #-4]!)
10410: e28db000 add fp, sp, #0
10414: e24dd02c sub sp, sp, #44 ; 0x2c
10418: e28bd000 add sp, fp, #0
1041c: e8bd0800 ldmfd sp!, {fp}
10420: e12fff1e bx lr
2.定长数组的栈空间
一步一步汇编进行分析:
1.先将fp入栈;
2.fp与sp同时指向一个地址;即栈空间为0;
3.扩展栈空间,大小为44个字节;//此处因为我们有个局部的定长数组
4.然后将栈空间还原为0;
5.将fp出栈
6.通过lr寄存器返回main函数;
可以看到的是,除了函数的出口与入口,在函数的运行过程中栈空间不会改变;
3.变长数组的使用
定长数组使用函数array_vla(int x),我们通过汇编进一步说明。
00010424 <array_vla>:
————————————————————————————(1)——————————————————————————————————
10424: e92d08f0 push {r4, r5, r6, r7, fp}
10428: e28db010 add fp, sp, #16
1042c: e24dd014 sub sp, sp, #20
—————————————————————————————(2)—————————————————————————————————
10430: e50b0020 str r0, [fp, #-32] ; 0xffffffe0
10434: e1a0300d mov r3, sp
10438: e1a0c003 mov ip, r3
1043c: e51b0020 ldr r0, [fp, #-32] ; 0xffffffe0
10440: e2403001 sub r3, r0, #1
10444: e50b301c str r3, [fp, #-28] ; 0xffffffe4
10448: e1a07000 mov r7, r0
1044c: e1a03007 mov r3, r7
10450: e3a04000 mov r4, #0
10454: e1a06284 lsl r6, r4, #5
10458: e1866da3 orr r6, r6, r3, lsr #27
1045c: e1a05283 lsl r5, r3, #5
10460: e1a05000 mov r5, r0
10464: e1a03005 mov r3, r5
10468: e3a04000 mov r4, #0
1046c: e1a02284 lsl r2, r4, #5
10470: e1822da3 orr r2, r2, r3, lsr #27
10474: e1a01283 lsl r1, r3, #5
10478: e1a03000 mov r3, r0
1047c: e1a03103 lsl r3, r3, #2
10480: e2833003 add r3, r3, #3
10484: e2833007 add r3, r3, #7
10488: e1a031a3 lsr r3, r3, #3
1048c: e1a03183 lsl r3, r3, #3
10490: e04dd003 sub sp, sp, r3
—————————————————————————————(3)—————————————————————————————————
10494: e1a0300d mov r3, sp
10498: e2833003 add r3, r3, #3
1049c: e1a03123 lsr r3, r3, #2
104a0: e1a03103 lsl r3, r3, #2
104a4: e50b3018 str r3, [fp, #-24] ; 0xffffffe8
104a8: e1a0d00c mov sp, ip
104ac: e24bd010 sub sp, fp, #16
104b0: e8bd08f0 pop {r4, r5, r6, r7, fp}
104b4: e12fff1e bx lr
4.变长数组的栈空间
可以明显的看到,虽然仅仅是一个定长数组与变长数组的差异,可以汇编代码其实差异是很大的,至少从汇编的行数上也能说明这一点,
对比定长数组的汇编分析,其主要可以分为三个过程:
1.开辟固定的栈空间
2.然后通过传参x计算出需要扩展的栈空间;
最终通过计算后利用r3进行扩展栈空间,通过反复尝试,发现r3 = (x+2)*sizeof(int);即分配的空间大于原始的数组空间,后面两个字节其实是编译器做的数组越界保护;
3.还原栈空间;
主要的差异为第二阶段的扩展栈空间的过程,从以上分析可以看到,其实变长数组只是对之前的栈做了动态的扩充,以达到可以实现栈变长的目地。
5 数组越界保护
无论是定长数组或者变长数组,发现在数组栈的申请时,会比原始的数组大一点,其实这是编译器做的数组越界保护,我们知道了这个原理后,其实就可以做相应的程序来"欺骗"过执行行为。
//test1.c
#include<stdio.h>
int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
a[10]= 111;
for(int i = 0; i < 11; ++i)
printf("%d ", a[i]);
printf("\n");
return 0;
}
//执行结果:
1 2 3 4 5 6 7 8 9 10 111
*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)
//test2.c
#include<stdio.h>
int x = 0;
int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
x = a[10];
a[10]= 111;
for(int i = 0; i < 11; ++i)
printf("%d ", a[i]);
printf("\n");
a[10] = x;
return 0;
}
//执行结果:
1 2 3 4 5 6 7 8 9 10 111
对于test1.c,很明显可以发现我们对a[10]进行了写操作,然后执行时出现了异常,我们对test1.c进行简单的修改,将a[10]的参数进行写入后最后进行恢复,发现test2.c是可以执行的,说明我们已经完美的欺骗过了执行环境;