一个简单的 ARM 汇编程序和 C 程序及深入分析

芯片框架

本实验开发板所用 ARM 芯片是 S3C2440 芯片,其框架如下:


01_010 S3C2440框架 .png

S3C2440 芯片是从 0 地址开始启动的,有 Nor 启动和 Nand 启动两种启动方式。

Nor 启动时,Nor Flash 的基地址为 0,片内 SDRAM 地址为0x4000 0000 (见芯片手册 Memory Map)。启动后,CPU 从Nor Flash 0 地址读出第 1 个指令(前4字节),执行;CPU 继续读出其它指令执行。

Nand 启动时,片内 4kSDRAM 基地址为 0,Nor Flash 不可访问。S3C2440 芯片硬件会把 Nand Flash 前 4K 内容复制到片内的SDRAM,然后 CPU 从0地址取出第1条指令执行。

原理图确定引脚

第一,从原理图确定 LCD 的引脚。


01_007 LED电路图.png

引脚 nLED1、2、4 输出高电平时灯熄灭,低电平时灯点亮。

GPF 寄存器地址和设置说明:


01_009 GPF 寄存器.png

所以,要控制 LED1 :

  • 需要设置 GPFCON[9:8] = 0b01, 把 GPF4 配置为输出引脚。即把 0x100 写入 GPFCON 这个寄存器,地址为0x5600 0050
  • 控制 LED 的亮灭,需要设置 GPFDAT[4] = 1 或者0,输出高电平或低电平。即把 0x10 或 0x0 写到地址 0x5600 0054

注: 上面的写法会破坏寄存器的其它位,其它位是控制其它引脚的。但为了让第一个裸板程序尽可能的简单,此实验才简单粗暴的这样处理。实际要用位操作,只设置对应的位!

编写汇编程序

汇编程序 led_on.S 代码如下:

/*
 * 点亮LED1: gpf4
 */
.text
.global _start

_start:

/* 配置GPF4为输出引脚
 * 把0x100写到地址0x56000050
 */
    ldr r1, =0x56000050
    ldr r0, =0x100  /* mov r0, #0x100 */
    str r0, [r1]

/* 设置GPF4输出低电平 
 * 把0写到地址0x56000054
 */
    ldr r1, =0x56000054
    ldr r0, =0  /* mov r0, #0 */
    str r0, [r1]

    /* 死循环 */
halt:
    b halt

Makefile:

all:
    arm-linux-gcc -c -o led_on.o led_on.S  //编译
    arm-linux-ld -Ttext 0 led_on.o -o led_on.elf  //链接
    arm-linux-objcopy -O binary -S led_on.elf led_on.bin  //生成bin文件
    arm-linux-objdump -D led_on.elf > led_on.dis  //反汇编
clean:
    rm *.bin *.o *.elf *.dis

编译后烧写到开发板上,即可看到LED1亮。

分析反汇编代码

代码中的ldr r1, =0x56000050这条伪指令的真实指令时什么呢?

可以通过反汇编来查看。生成的 led_on.dis 就是反汇编文件。led_on.dis 如下:

led_on.elf:     file format elf32-littlearm

Disassembly of section .text:

00000000 <_start>:
   0:   e59f1014    ldr r1, [pc, #20]   ; 1c <.text+0x1c>
   4:   e3a00c01    mov r0, #256    ; 0x100
   8:   e5810000    str r0, [r1]
   c:   e59f100c    ldr r1, [pc, #12]   ; 20 <.text+0x20>
  10:   e3a00000    mov r0, #0  ; 0x0
  14:   e5810000    str r0, [r1]

00000018 :
  18:   eafffffe    b   18 
  1c:   56000050    undefined
  20:   56000054    undefined
  
  //第一列是地址,第二列是机器码,第三列是汇编;
  //在反汇编文件里可以看到,ldr r1, =0x56000050 被转换成 ldr r1, [pc, #20], pc+20地址的值为0x56000050,通过这种方式为r1赋值。 对于立即数0x100而言,ldr r0,=0x100即是转换成了mov r0,#256;

上面的反汇编程序解析如下:

0:  e59f1014    ldr r1, [pc, #20]   ; 1c <.text+0x1c>
// r1 = [pc + 20] = [0 + 8 + 20] = [0x1c] = 0x56000050
4:  e3a00c01    mov r0, #256    ; 0x100
8:  e5810000    str r0, [r1]
// 把r0 即0x100写r1对应的内存,即0x100写到地址0x56000050
c:  e59f100c    ldr r1, [pc, #12]   ; 20 <.text+0x20>
// r1 = [pc + 12] = [0xc + 8 + 12] = [32] = [0x20] = 56000054
10: e3a00000    mov r0, #0  ; 0x0
14: e5810000    str r0, [r1]

00000018 :
  18:   eafffffe    b   18 
  1c:   56000050    undefined
  20:   56000054    undefined

编写 C 程序

lcd.c

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

    /*配置GPF4为输出引脚*/
    *pGPFCON = 0x100;
    
    /*配置GPF4输出0*/
    *pGPFDAT = 0;

    return 0;
}

还需要写一个汇编程序, 给 main 函数设置内存, 调用main函数。

start.S:

.text
.global _start
_start:
    /*设置内存:sp栈*/
    ldr sp,=4096 /*nand启动*/
        
//  ldr sp, =0x40000000 /*nor启动*/

    /*调用main*/
    bl main
halt:
    b halt

bl main 跳转执行main函数,并把"返回地址"(即下一条指令地址)保存在lr寄存器中,这里返回地址是halt。

Makefile:

all:
    arm-linux-gcc -c -o led.o led.c
    arm-linux-gcc -c -o start.o start.S
    arm-linux-ld -Ttext 0 start.o led.o -o led.elf
    arm-linux-objcopy -O binary -S led.elf led.bin
    arm-linux-objdump -D led.elf > led.dis
clean:
    rm *.bin *.o *.elf *.dis

最后即可编译,烧写到开发板,看到LED1亮。

用反汇编代码分析C程序的执行过程

汇编程序 start.S所做的事情是 :1. 设置栈;2. 调用 main,并把返回值地址保存到 lr 中。

C程序 led.c 的 main() 所做的事情是 :1. 定义2个局部变量;2. 设置变量;3. return 0。

从反汇编代码来分析一下C程序执行过程,解答下面的问题。

  1. 为什么要设置栈?因为c函数要用。
  2. 栈的作用是什么? a.保存局部变量;b.保存 lr、fp、ip、sp、pc 等寄存器的值;
  3. 调用者如何传参数给被调用者?
  4. 被调用者如何传返回值给调用者?
  5. 怎么从栈中恢复那些寄存器?

ATPCS规则

在arm中有个ATPCS规则,约定 r0 - r15 寄存器的用途。即ARM-THUMB procedure call standard(ARM-Thumb子程序调用规则)

r0 - r3: 用于调用者和被调用者之间传参数;

r4-r11: 可能被子函数使用,所以在函数的入口保存它们,在函数的出口恢复它们;

反汇编程序 led.dis :

led.elf:     file format elf32-littlearm

Disassembly of section .text:

00000000 <_start>:
   0:   e3a0da01    mov sp, #4096   ; 0x1000
   4:   eb000000    bl  c 
00000008 : 8: eafffffe b 8 0000000c
: 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.

Nand 启动时,硬件机制会把 nand flash 上前 4K 的机器码拷贝到片内的 4K SDRAM上。然后从0地址开始执行。

开发板上电后,将从0地址开始执行,即开始执行:

mov sp, #4096  //:设置栈地址在4k RAM的最高处,sp=4096;
bl    c 
//:跳到c地址处的main函数,并保存下一行代码地址到lr,即lr=8; mov ip, sp //:给ip赋值sp的值,ip=sp=4096 stmdb sp!, {fp, ip, lr, pc} //:按高编号寄存器存在高地址,依次将pc、lr、ip、fp存入sp-4中; sub fp, ip, #4 //:fp的值为ip-4=4096-4=4092; sub sp, sp, #8 //:sp的值为sp-8=(4096-4x4)-8=4072; mov r3, #1442840576 //:r3赋值0x5600 0000; add r3, r3, #80 //:r3的值加0x50,即r3=0x5600 0050; str r3, [fp, #-16] //:r3存入[fp-16]所在的地址,即地址4076处存放0x5600 0050; mov r3, #1442840576 //:r3赋值0x5600 0000; add r3, r3, #84 //:r3的值加0x54,即r3=0x5600 0054; str r3, [fp, #-20] //:r3存入[fp-20]所在的地址,即地址4072处存放0x5600 0054; ldr r2, [fp, #-16] //:r2取[fp-16]地址处的值,即[4076]地址的值,r2=0x5600 0050; mov r3, #256 //:r3赋值为0x100; str r3, [r2] //:将r3写到r2内容所对应的地址,即0x5600 0050地址处的值为0x100;;对应c语言*pGPFCON = 0x100;; ldr r2, [fp, #-20] //:r2取[fp-20]地址处的值,即[4072]地址的值,r2=0x5600 0054; mov r3, #0 //:r3赋值为0x00; str r3, [r2] //:将r3写到r2内容所对应的地址,即0x5600 0054地址处的值为0x00;对应c语言*pGPFDAT = 0; mov r3, #0 //:r3赋值为0x00; mov r0, r3 //:r0=r3=0x00; sub sp, fp, #12 //:sp=fp-12=4092-12=4080; ldmia sp, {fp, sp, pc} //:从栈中恢复寄存器,fp=4080地址处的值=原来的fp,sp=4084地址处的值=4096,pc=4088地址处的值=8,随后跳到0x08地址处继续执行。 //注意点: 调用main函数之前sp = 4096, main函数返回后sp仍然是4096

汇编程序传递参数给C程序分析

前面的例子,汇编程序调用 main.c 并没有传递参数,这里修改下 c 程序,添加带参数的函数。

led.c:

void delay(volatile int d) //这里volatile关键字是为了避免编译器自作聪明的把delay函数优化掉. 编译器可能从逻辑上看,省掉delay函数也没什么关系。
{
    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;
}

start.S:

.text
.global _start

_start:

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

    mov r0, #4    //r0-r3:调用者和被调用者之间传参数;
    bl led_on

    ldr r0, =100000
    bl delay

    mov r0, #5
    bl led_on

halt:
    b halt

led.elf:

分析见注释:

led.elf:     file format elf32-littlearm

Disassembly of section .text:

00000000 <_start>:
   0:   e3a0da01    mov sp, #4096   ; 0x1000   //:设置栈地址在4k RAM的最高处,sp=4096;
   4:   e3a00004    mov r0, #4  ; 0x4  //:r0=4,作为参数;
   8:   eb000012    bl  58   //:调到58地址处的led_on函数,并保存下一行代码地址到lr,即lr=8;在led_on中会使用到r0;
   c:   e59f000c    ldr r0, [pc, #12]   ; 20 <.text+0x20>  //:r0=[pc+12]处的值=[0xc+8+12]=0x20 处的值 = 0x186a0 = 1000000,作为参数;
  10:   eb000003    bl  24  //:调用24地址处的delay函数,并保存下一行代码地址到lr,即lr=24;在delay中会使用到r0;
  14:   e3a00005    mov r0, #5  ; 0x5 //:r0=5,作为参数;
  18:   eb00000e    bl  58  //:调到58地址处的led_on函数,并保存下一行代码地址到lr,即lr=58;在led_on中会使用到r0;

0000001c :
  1c:   eafffffe    b   1c 
  20:   000186a0    andeq   r8, r1, r0, lsr #13

00000024 :
  24:   e1a0c00d    mov ip, sp
  28:   e92dd800    stmdb   sp!, {fp, ip, lr, pc}
  2c:   e24cb004    sub fp, ip, #4  ; 0x4
  30:   e24dd004    sub sp, sp, #4  ; 0x4
  34:   e50b0010    str r0, [fp, #-16]
  38:   e51b3010    ldr r3, [fp, #-16]
  3c:   e2433001    sub r3, r3, #1  ; 0x1
  40:   e50b3010    str r3, [fp, #-16]
  44:   e51b3010    ldr r3, [fp, #-16]
  48:   e3730001    cmn r3, #1  ; 0x1
  4c:   0a000000    beq 54 
  50:   eafffff8    b   38 
  54:   e89da808    ldmia   sp, {r3, fp, sp, pc}

00000058 :
  58:   e1a0c00d    mov ip, sp
  5c:   e92dd800    stmdb   sp!, {fp, ip, lr, pc}
  60:   e24cb004    sub fp, ip, #4  ; 0x4
  64:   e24dd00c    sub sp, sp, #12 ; 0xc
  68:   e50b0010    str r0, [fp, #-16]
  6c:   e3a03456    mov r3, #1442840576 ; 0x56000000
  70:   e2833050    add r3, r3, #80 ; 0x50
  74:   e50b3014    str r3, [fp, #-20]
  78:   e3a03456    mov r3, #1442840576 ; 0x56000000
  7c:   e2833054    add r3, r3, #84 ; 0x54
  80:   e50b3018    str r3, [fp, #-24]
  84:   e51b3010    ldr r3, [fp, #-16]
  88:   e3530004    cmp r3, #4  ; 0x4
  8c:   1a000003    bne a0 
  90:   e51b2014    ldr r2, [fp, #-20]
  94:   e3a03c01    mov r3, #256    ; 0x100
  98:   e5823000    str r3, [r2]
  9c:   ea000005    b   b8 
  a0:   e51b3010    ldr r3, [fp, #-16]
  a4:   e3530005    cmp r3, #5  ; 0x5
  a8:   1a000002    bne b8 
  ac:   e51b2014    ldr r2, [fp, #-20]
  b0:   e3a03b01    mov r3, #1024   ; 0x400
  b4:   e5823000    str r3, [r2]
  b8:   e51b3018    ldr r3, [fp, #-24]
  bc:   e3a02000    mov r2, #0  ; 0x0
  c0:   e5832000    str r2, [r3]
  c4:   e3a03000    mov r3, #0  ; 0x0
  c8:   e1a00003    mov r0, r3
  cc:   e24bd00c    sub sp, fp, #12 ; 0xc
  d0:   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.

你可能感兴趣的:(一个简单的 ARM 汇编程序和 C 程序及深入分析)