第1课-栈初始化

概念解析

栈是一种具有后进先出性质的数据组织方式,栈底是第一个入栈数据所处的位置,一般用BP指针表示;栈顶是最后一个进栈数据所处的位置,一般用SP指针表示。

根据SP指针指向的位置,栈可以分为满栈和空栈:
满栈:堆栈指针SP总是指向最后压入堆栈的数据
空栈:堆栈指针SP总是指向下一个将要放入数据的位置
Alt text

ARM采用满栈。

根据SP指针移动的方向,栈可以分为升栈和降栈:
升栈:随着数据的入栈,SP指针从低地址->高地址
降栈:随着数据的入栈,SP指针从高地址->低地址
Alt text

ARM采用降栈。

栈帧

简单来说,栈帧(stack frame)就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。栈帧的两个边界分别由fp(r11)和sp(r13)来限定,其他还有pc(r15),lr(r15)。理论上来说ARM的15个通用寄存器是通用的,但实际并不是这样,以上这些寄存器在函数调用过程中所起的作用由APCS(ARM过程调用标准)决定。
Alt text

栈的作用

在C语言中栈有以下几个作用:

  1. 保存局部变量
  2. 传递参数
  3. 保存寄存器的值

下面通过分析一个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 ,回到子程序的返回地址

Alt text

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处开始安排栈

第2课-初始化BSS段

BSS段用于存储程序中未初始过的全局变量,这个段的内容并不会出现在程序的可执行文件中,但是会在装载的过程中为bss段的内容分配空间。为bss段分配的空间要进行一次清零,这也是为什么位于bss段的变量值为0的原因。初始化bss段,也就是完成这个清零操作。

注意,清空bss段需要知道bss段的起始和结束地址,这个地址是在链接脚本中用相关变量描述的,在链接脚本中有如下内容:
Alt text

则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

第3课-一跃进入C大门

这部分将讲解的是如何像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;
}

第4课-C与汇编混合编程

汇编调用C函数

直接将函数名赋值给pc指针即可。

ldr pc, =gboot_main

C调用汇编函数

汇编的语句标号加上括号即可。
汇编:

.global light_led   @将汇编语句标号声明为全局符号
light_led:
...

C语言:

light_led();

C内嵌汇编代码

格式:

__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"
);

  • 无标签