码迷,mamicode.com
首页 > 编程语言 > 详细

C语言版——点亮LED灯,深入到栈

时间:2017-10-30 23:55:59      阅读:502      评论:0      收藏:0      [点我收藏+]

标签:sas   十分   ima   out   sub   重点   结构   示例   后缀   

在上一篇进行了汇编语言的编写之后,我们采用C语言来编写程序,毕竟C语言才是我们使用最多的语言。

仅仅是点亮LED灯显然太过于简单,我们需要分析最后的反汇编,了解函数调用栈,深入C语言骨髓去分析代码,并且自己编写C语言的库函数版本,方便以后开发,同时也是对自己C语言封装能力的锻炼。

先贴韦老大的代码:

start.s:

.text
.global _start

_start:

    /* 设置内存: sp 栈 */
    ldr sp, =4096  /* nand启动 */
//    ldr sp, =0x40000000+4096  /* nor启动 */

    /* 调用main */
    bl main

halt:
    b halt

LED.c:

int main()
{
    unsigned int *pGPFCON = (unsigned int *)0x56000050;
    unsigned int *pGPFDAT = (unsigned int *)0x56000054;
    /* 配置GPF4为输出引脚 */
    *pGPFCON = 0x100;
    
    /* 设置GPF4输出0 */
    *pGPFDAT = 0;
        
    return 0;
}

C语言操作,的传统IDE开发当中,我们只用从main函数开始写代码就行了,但是IDE隐藏了太多技术细节。

我们在arm嵌入式linux开发过程中,都需要自己来,这对初学者是不友好,但是对深入学习却是很有帮助的。

首先,第一点,nand flash启动,使用片内4k sram。

我们都知道,函数调用和局部变量的存储需要使用到一种叫做栈的数据结构。

这里说明,s3c2440,采用默认的栈生长方式,这也是我们最常见的方式,高地址往低地址生长。

要调用main函数,我们需要开辟栈,这里使用片内4k 内存作为栈。

看看反汇编:

 

led.elf:     file format elf32-littlearm

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0da01     mov    sp, #4096    ; 0x1000
   4:    eb000000     bl    c <main>

00000008 <halt>:
   8:    eafffffe     b    8 <halt>

0000000c <main>:
   c:    e1a0c00d     mov    ip, sp
  10:    e92dd800     stmdb    sp!, {fp, ip, lr, pc}
  14:    e24cb004     sub    fp, ip, #4    ; 0x4
  18:    e24dd008     sub    sp, sp, #8    ; 0x8
  1c:    e3a03456     mov    r3, #1442840576    ; 0x56000000
  20:    e2833050     add    r3, r3, #80    ; 0x50
  24:    e50b3010     str    r3, [fp, #-16]
  28:    e3a03456     mov    r3, #1442840576    ; 0x56000000
  2c:    e2833054     add    r3, r3, #84    ; 0x54
  30:    e50b3014     str    r3, [fp, #-20]
  34:    e51b2010     ldr    r2, [fp, #-16]
  38:    e3a03c01     mov    r3, #256    ; 0x100
  3c:    e5823000     str    r3, [r2]
  40:    e51b2014     ldr    r2, [fp, #-20]
  44:    e3a03000     mov    r3, #0    ; 0x0
  48:    e5823000     str    r3, [r2]
  4c:    e3a03000     mov    r3, #0    ; 0x0
  50:    e1a00003     mov    r0, r3
  54:    e24bd00c     sub    sp, fp, #12    ; 0xc
  58:    e89da800     ldmia    sp, {fp, sp, pc}
Disassembly of section .comment:

00000000 <.comment>:
   0:    43434700     cmpmi    r3, #0    ; 0x0
   4:    4728203a     undefined
   8:    2029554e     eorcs    r5, r9, lr, asr #10
   c:    2e342e33     mrccs    14, 1, r2, cr4, cr3, {1}
  10:    Address 0x10 is out of bounds.

 

说明:

<.comment>:在上面的反汇编当中,它不是汇编代码的一部分,是注解,给我们一些提示,便于我们阅读、理解的。在讲解这个汇编之前,我们需要先看一个arm寄存器的别名表。

技术分享

好,现在开始分析,其中最重要的也是新出现的两个指令:

stmdb,ldmia

详细介绍可以参见韦老大书籍P53,arm嵌入式系统开发P58。

简单说明,db表示事先递减方式,ia表示事后递增方式。

 先对反汇编进行注解:

  led.elf:     file format elf32-littlearm
  
  Disassembly of section .text:
  
  00000000 <_start>:
    0:    e3a0da01     mov    sp, #4096    ; 赋值4096给sp堆栈寄存器
    4:    eb000000     bl    c <main>    ;跳转到main函数,同时保存main函数返回地址到lr寄存器,返回地址为下一条指令的地址,这里lr应为8
  
  00000008 <halt>:
   8:    eafffffe     b    8 <halt>;死循环
 
 0000000c <main>:
    c:    e1a0c00d     mov    ip, sp;备份sp寄存器的值到ip,ip是r12寄存器的别名
  10:    e92dd800     stmdb    sp!, {fp, ip, lr, pc}
; 这条指令需要重点讲解,首先,fp,ip,lr,pc分别对应寄存器:r11,r12,r14,r15
; stm指令多寄存器操作的时候,后面的寄存器从编号高的先开始存储,把后面几个寄存器的值写入sp所对应的内存块中,(注意和但寄存器操作区分开来)后缀db表示 先减后存储
; sp后面加了一个感叹号!,表示sp最后的值等于最后被修改的值,不加感叹号就算操作使sp更改过,sp也等于最初的值
; 首先操作r15,即pc,sp先减,sp=sp-4,此时sp=4092,pc等于当前指令地址加8,即此时pc=0x10+8=0x18,相当于把0x18写入sram地址4092
; 同理操作r14,即lr,sp=sp-4,lr=8,相当于把0x8写入4088地址
; r12,ip是等于4096的,上面的move指令,r11,fp的值此时未定
  14:    e24cb004     sub    fp, ip, #4    ; fp=ip-4=4092
  18:    e24dd008     sub    sp, sp, #8    ; sp=sp-8=4072
  1c:    e3a03456     mov    r3, #1442840576    ;#1442840576等于十六机制0x56000000,把这个数存放在r3中
  20:    e2833050     add    r3, r3, #80    ;r3=r3+80=0x56000050
  24:    e50b3010     str    r3, [fp, #-16];fp-16=4076,把0x56000050放入内存4076中
  28:    e3a03456     mov    r3, #1442840576    ; 0x56000000
  2c:    e2833054     add    r3, r3, #84    ; r3=0x56000054
  30:    e50b3014     str    r3, [fp, #-20];fp=fp-20=4072,把0x56000054放入内存4072中
  34:    e51b2010     ldr    r2, [fp, #-16];r2=[fp-16=4076]=0x56000050
  38:    e3a03c01     mov    r3, #256; r3==256=0x100
  3c:    e5823000     str    r3, [r2];把0x100存入[0x56000050]
  40:    e51b2014     ldr    r2, [fp, #-20];r2=[fp-20=4072]=0x56000054
  44:    e3a03000     mov    r3, #0    ; r3=0=0x0
  48:    e5823000     str    r3, [r2];把0x0存入0x56000054内存中
  4c:    e3a03000     mov    r3, #0    ; r3=0x0
  50:    e1a00003     mov    r0, r3;r0=r3=0x0,这里编译器有点笨,可以直接r0给0的,这里对应return 0.
  54:    e24bd00c     sub    sp, fp, #12    ; sp=fp-12=4080
  58:    e89da800     ldmia    sp, {fp, sp, pc}
  ;恢复保存的现场,ldmia,事后增加,从sp所对应内存块中取出数据存放到后面的寄存器,高编号的寄存器放在高地址,低编号的寄存器
  ;放在低地址,此时sp=4080,从低地址往高地址开始恢复,这也符合ia后缀
  ;先恢复fp,此时4080地址存放的值,注意是值不是地址,等于4092,恰好就是等于fp,即一顿操作之后,fp还是等于原来的fp
  ;再恢复sp,sp=sp+4=4084,内存4084对应的值是4096,即一顿操作之后,sp又等于4096了;
  ;最后恢复pc,sp=sp+4=4088,内存4088对应的值是8,即一段操作之后,pc=8了,pc等于8意味着什么?意味着函数从main函数返回了,将去执行那个死循环halt

为了更好的理解函数入栈,让我们深入理解C语言底层汇编,画出内存示意图:

技术分享

你可能会说。0-4096不是4097了吗?这样问非常好,不放过任何有疑问的细节,但是,4096,可以是4096的开始,也可以是4095的结尾,这里表示的是4095的结尾,因为4096我们一来是要先减4的。上图的4096是没用使用到真正属于4096地址后扩4字节的,而是在刚好到达4096时,之前的内存。

到这里,终于完成了一大半,我们知道了函数入栈之后,似乎函数调用的参数传递,也要用到栈啊,那么我们继续挖掘汇编。这里又需要补充几点arm方面的知识。

技术分享

技术分享

技术分享

技术分享

现在编写汇编代码, 传递一个参数:

.text
.global _start

_start:

    /* 设置内存: sp 栈 */
    ldr sp, =4096  /* nand启动 */
//    ldr sp, =0x40000000+4096  /* nor启动 */

    mov r0, #4
    bl led_on

    ldr r0, =100000
    bl delay

    mov r0, #5
    bl led_on

halt:
    b halt

对应的c代码:

void delay(volatile int d)
{
    while (d--);
}

int led_on(int which)
{
    unsigned int *pGPFCON = (unsigned int *)0x56000050;
    unsigned int *pGPFDAT = (unsigned int *)0x56000054;

    if (which == 4)
    {
        /* 配置GPF4为输出引脚 */
        *pGPFCON = 0x100;
    }
    else if (which == 5)
    {
        /* 配置GPF5为输出引脚 */
        *pGPFCON = 0x400;
    }
    
    /* 设置GPF4/5输出0 */
    *pGPFDAT = 0;

    return 0;
}

上面汇编中,只对应一个参数,所以只用r0就可以达到函数参数传递的效果,汇编比较简单就不赘述了。

这个例子是为了让我们了解汇编通过寄存器传递函数参数,前提是必须先设置sp寄存器。

甚至不必编写main函数,当然不建议这样做,这样示例是为让你明白一点,main函数也是因为启动代码去调用了它,我们通过改写启动代码,可以没有main函数。

 终于到了我们最熟悉的阶段了,编写C语言应用程序,一般来说,复杂的和可复用性更高的代码我们肯定是用C语言编写,全用汇编编写代码真是很慢而且麻烦,更不用说机器码编程了,但是了解它们对我们深入学习又十分有用,这或许就是arm对初学者不友好的原因吧,因为现在为止我们还没有大型项目需要编写复杂的Makefile,后面还有很多技能需要get,这也是在买了板子大半年了才真的开始上手的原因,需要花时间补习其他知识。

言归正传,如上面的C语言程序,虽然完成了要求,可是复用性太差,既然你觉得你C语言最熟悉,那么就请封装一个复用性高的代码出来看看吧。

/*明天补c代码,先洗漱休息了*/

C语言版——点亮LED灯,深入到栈

标签:sas   十分   ima   out   sub   重点   结构   示例   后缀   

原文地址:http://www.cnblogs.com/yangguang-it/p/7758167.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!