栈是一种具有后进先出性质的数据组织方式,栈底是第一个入栈数据所处的位置,一般用BP指针表示;栈顶是最后一个进栈数据所处的位置,一般用SP指针表示。
根据SP指针指向的位置,栈可以分为满栈和空栈:
满栈:堆栈指针SP总是指向最后压入堆栈的数据
空栈:堆栈指针SP总是指向下一个将要放入数据的位置
ARM采用满栈。
根据SP指针移动的方向,栈可以分为升栈和降栈:
升栈:随着数据的入栈,SP指针从低地址->高地址
降栈:随着数据的入栈,SP指针从高地址->低地址
ARM采用降栈。
简单来说,栈帧(stack frame)就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。栈帧的两个边界分别由fp(r11)和sp(r13)来限定,其他还有pc(r15),lr(r15)。理论上来说ARM的15个通用寄存器是通用的,但实际并不是这样,以上这些寄存器在函数调用过程中所起的作用由APCS(ARM过程调用标准)决定。
在C语言中栈有以下几个作用:
下面通过分析一个C语言程序的反汇编代码来验证栈的这几个作用。
#include void func(int a,int b,int c,int d,int e,int f) { int k; k = e + f; } void func2(int a,int b) { int k; k = a + b; } void func1(int a,int b) { int c; func2(3,4); c = a + b; } int main() { int a = 1; int b = 2; func(1, 2, 3, 4, 5, 6); func1(1, 2); return 0; } |
对这段代码进行测试,然后将反汇编文件输出到stack.dump,分析汇编文件的生成方式:
arm-linux-gcc -g stack.c -o stack arm-linux-objdump -D -S stack > stack.dump |
反汇编文件的关键代码如下所示:
int main() { 83f0: e92d4800 push {fp, lr} @将fp和lr压栈后,sp指令会向下移动两个字的长度 83f4: e28db004 add fp, sp, #4 ; 0x4 @划定main函数栈的空间 83f8: e24dd010 sub sp, sp, #16 ; 0x10 int a = 1; 83fc: e3a03001 mov r3, #1 ; 0x1 @main函数的局部变量保存在main函数的栈帧上 8400: e50b300c str r3, [fp, #-12] int b = 2; 8404: e3a03002 mov r3, #2 ; 0x2 8408: e50b3008 str r3, [fp, #-8] func(1, 2, 3, 4, 5, 6); 840c: e3a03005 mov r3, #5 ; 0x5 @四个以上的参数的传递需要借助栈 8410: e58d3000 str r3, [sp] 8414: e3a03006 mov r3, #6 ; 0x6 8418: e58d3004 str r3, [sp, #4] 841c: e3a00001 mov r0, #1 ; 0x1 @前四个参数直接用寄存器r0~r1来传递 8420: e3a01002 mov r1, #2 ; 0x2 8424: e3a02003 mov r2, #3 ; 0x3 8428: e3a03004 mov r3, #4 ; 0x4 842c: ebffffc6 bl 834c func1(1, 2); 8430: e3a00001 mov r0, #1 ; 0x1 @四个以下的参数直接用r0~r3来传送 8434: e3a01002 mov r1, #2 ; 0x2 8438: ebffffdd bl 83b4 return 0; 843c: e3a03000 mov r3, #0 ; 0x0 8440: e1a00003 mov r0, r3 8444: e24bd004 sub sp, fp, #4 ; 0x4 8448: e8bd4800 pop {fp, lr} 844c: e12fff1e bx lr |
void func(int a,int b,int c,int d,int e,int f) { 834c: e52db004 push {fp} ; (str fp, [sp, #-4]!) 8350: e28db000 add fp, sp, #0 ; 0x0 @划定func函数栈的空间 8354: e24dd01c sub sp, sp, #28 ; 0x1c 8358: e50b0010 str r0, [fp, #-16] @将r0~r3中的参数值保存到func函数的栈帧中 835c: e50b1014 str r1, [fp, #-20] 8360: e50b2018 str r2, [fp, #-24] 8364: e50b301c str r3, [fp, #-28] int k; k = e + f; 8368: e59b3004 ldr r3, [fp, #4] @将第5、6个参数保存到r2 r3寄存器中 836c: e59b2008 ldr r2, [fp, #8] 8370: e0833002 add r3, r3, r2 8374: e50b3008 str r3, [fp, #-8] @将计算结果存放到func函数的栈帧中 } 8378: e28bd000 add sp, fp, #0 ; 0x0 @sp指向func函数栈帧的fp位置 837c: e8bd0800 pop {fp} @弹出fp,参考下面栈帧的示意图,fp又回到了main函数栈帧的fp位置 8380: e12fff1e bx lr @等效到mov pc, lr ,回到子程序的返回地址 |
000083b4 : void func1(int a,int b) { 83b4: e92d4800 push {fp, lr} 83b8: e28db004 add fp, sp, #4 ; 0x4 83bc: e24dd010 sub sp, sp, #16 ; 0x10 83c0: e50b0010 str r0, [fp, #-16] @下一步将调用func2,所以需要将r0,r1的值保存在func1的栈上 83c4: e50b1014 str r1, [fp, #-20] @以便于在调用func2时再次使用r0,r1来传递参数 int c; func2(3,4); 83c8: e3a00003 mov r0, #3 ; 0x3 83cc: e3a01004 mov r1, #4 ; 0x4 83d0: ebffffeb bl 8384 c = a + b; 83d4: e51b3010 ldr r3, [fp, #-16] 83d8: e51b2014 ldr r2, [fp, #-20] 83dc: e0833002 add r3, r3, r2 83e0: e50b3008 str r3, [fp, #-8] } 83e4: e24bd004 sub sp, fp, #4 ; 0x4 83e8: e8bd4800 pop {fp, lr} 83ec: e12fff1e bx lr 00008384 : void func2(int a,int b) { 8384: e52db004 push {fp} ; (str fp, [sp, #-4]!) 8388: e28db000 add fp, sp, #0 ; 0x0 838c: e24dd014 sub sp, sp, #20 ; 0x14 8390: e50b0010 str r0, [fp, #-16] 8394: e50b1014 str r1, [fp, #-20] int k; k = a + b; 8398: e51b3010 ldr r3, [fp, #-16] 839c: e51b2014 ldr r2, [fp, #-20] 83a0: e0833002 add r3, r3, r2 83a4: e50b3008 str r3, [fp, #-8] } 83a8: e28bd000 add sp, fp, #0 ; 0x0 83ac: e8bd0800 pop {fp} 83b0: e12fff1e bx lr |
栈的初始化只需对sp指针进行赋值即可,以下代码将sp指向第64MB内存空间。
stack_init: ldr sp, =0x54000000 @从内存的64MB处开始安排栈 |
BSS段用于存储程序中未初始过的全局变量,这个段的内容并不会出现在程序的可执行文件中,但是会在装载的过程中为bss段的内容分配空间。为bss段分配的空间要进行一次清零,这也是为什么位于bss段的变量值为0的原因。初始化bss段,也就是完成这个清零操作。
注意,清空bss段需要知道bss段的起始和结束地址,这个地址是在链接脚本中用相关变量描述的,在链接脚本中有如下内容:
则bss_start和bss_end就是bss段的起始和结束地址,初始化bss段的代码如下:
clear_bss: ldr r0, =bss_start ldr r1, =bss_end cmp r0, r1 moveq pc, lr @bss段为空,不需要清空 clear_loop: mov r2, #0 str r2, [r0], #4 cmp r0, r1 bne clear_loop |
这部分将讲解的是如何像uboot的第二启动阶段那样从汇编语言跳转到C语言。我们一般认为C语言的代码位于内存中,而汇编代码则位于ISRAM中,因此,跳转的方式必须要使用绝对跳转,而不是B和BL这样的相对跳转。
注意,之前的程序有一步是完成代码的拷贝,拷贝的具体内容是把位于ISRAM上的启动代码拷贝到内存,所以内存和ISRAM上都有相同的代码,使用B和BL的方式跳转或许可以让代码执行,但不是我们要测试的重点,我们要做的事情就是,跳转到内存中的那部分代码去执行,而不是ISRAM。要完成这个跳转只能使用绝对跳转。
跳转的语句如下,其中gboot_main是位于单独的c语言文件中的函数:
start.S:
ldr pc, =gboot_main @跳转到c语言中去执行 |
main.c:
#define GPMCON (*(volatile unsigned long *)0x7F008820) #define GPMDAT (*(volatile unsigned long *)0x7F008824) int gboot_main() { GPMCON = 0x1111; //点亮LED灯 GPMDAT = 0xe; return 0; } |
直接将函数名赋值给pc指针即可。
ldr pc, =gboot_main |
汇编的语句标号加上括号即可。
汇编:
.global light_led @将汇编语句标号声明为全局符号 light_led: ... |
C语言:
light_led(); |
格式:
__asm__( 汇编语句部分 :输出部分(可能修改的C语言中的变量) :输入部分(需要使用的C语言中的变量) :破坏描述部分(可能修改的寄存器的值) ); |
示例1:
void write_p15_c1(unsigned long value) { __asm__( "mcr p15, 0, %0, c1, c0, 0\n" : :"r"(value) @编译器选择一个R*寄存器 ); } |
示例2:
unsigned long read_p15_c1(void) { unsigned long value; __asm__( "mrc p15, 0, %0, c1, c0, 0\n" :"=r"(value) @"="表示只写操作数,用于输出部 : :"memory" ); return value; } |
示例3:使用volatile来告诉编译器不要进行优化
unsigned long old; unsigned long temp; __asm__ volatile( "mrs %0, cpsr \n" "orr %1, %0, #128\n" "msr cpsr_c, %1\n" :"=r"(old), "=r"(temp) : :"memory" ); |