【Raspberry Pi系列】4. ARM指令

来源:互联网 发布:整篇文章翻译软件 编辑:程序博客网 时间:2024/05/02 01:51

我们有必要了解我们手中的板子到底最根本的机制——汇编语言。树莓派是建立在ARM指令的基础上的。接下来的几篇文章并不试图从机器码的角度来详细分析ARM汇编的详细原理,而是通过实际的通过C语言反汇编的分析以及自行编写简单的汇编指令,来对ARM指令有一些基础的了解。

这里我们假设读者已经有了MIPS汇编基础或者Intel处理器汇编的基础。关于基本的运算指令、存储/读取指令、转移指令不会详细介绍。不过下面部分代码还是给出了部分汇编代码的注释,以供理解。


器材

硬件

  • 实验主板一块,本试验使用树莓派
  • 5V/1A电源一个
  • microUSB线一根

自备器材

  • PC(Linux)一台;
  • USB无线网卡;

软件

  • 交叉编译工具链。

步骤

1. 交叉编译之生成Thumb指令&ARM指令

Thumb是ARM体系结构中一种16位的指令集。Thumb指令集可以看作是ARM指令压缩形式的子集,它是为减小代码量而提出,具有16bit的代码密度。

Thumb指令体系并不完整,只支持通用功能,必要时仍需要使用ARM指令,如进入异常时。其指令的格式与使用方式与ARM指令集类似,而且使用并不频繁,Thumb指令集作一般了解。

这一步我们通过编译参数改变,相同的程序,ARM和Thumb编译的结果有何不同, 包括比较指令本身和整体目标代码的大小等。首先我们写一段用于交叉编译的代码:

#include <stdio.h>int main(){  int t = 72;  printf("%d Hello world!\n",t);}

然后复制为arm.c以及thumb.c来分别进行编译,首先对arm.c编译为ARM指令,使用指令:arm-bcm2708hardfp-linux-gnueabi-gcc -c arm.c

对thumb.c进行编译,这时我们需要设置对应的参数,使用命令arm-bcm2708hardfp-linux-gnueabi-gcc -c -mthumb -msoft-float thumb.c

此时使用交叉编译工具链中的objdump工具来进行反汇编,查看我们获得的对应的.o文件中的指令内容,使用指令分别查看,结果截图如下:

反汇编结果

返汇编结果

可以看到明显ARM指令是32位指令,编码为8位十六进制,而Thumb指令则是16位指令,从指令条数来看Thumb是要比ARM要复杂,但是从整体的文件大小来看,是Thumb要比较小:
arm&thumb比较

2. ARM 指令中的条件执行指令

下面我们来对指令中的条件执行指令,查看具体翻译之后的结果观察是否出现我们想要看到的指令。首先编写一段具有条件执行指令的代码:

#include <stdio.h>int main(){        int a=72,b=15;        int res;        if(a>b)                res = a-b;        else                res = b-a;}

编译后反汇编得到代码如下图所示:
结果

此时观察到ble指令出现,整个流程的注释可以如下所示:

00000000 <main>:   0:   e52db004    push {fp}          ;(str fp, [sp, #-4]!)  保护fp帧指针   4:   e28db000    add fp, sp, #0     ;将sp的值赋给fp,保护sp指针值   8:   e24dd014    sub sp, sp, #20    ;栈增长5个字节   c:   e3a03048    mov r3, #72        ;0x48,将r3寄存器赋值为72  10:   e50b3008    str r3, [fp, #-8]  ;将r3的值(72)存到fp-8  14:   e3a0300f    mov r3, #15        ;将r3寄存器赋值位15  18:   e50b300c    str r3, [fp, #-12] ;存入fp-12  1c:   e51b2008    ldr r2, [fp, #-8]  ;将fp-8的值载入r2=72  20:   e51b300c    ldr r3, [fp, #-12] ;r3=15  24:   e1520003    cmp r2, r3         ;比较r2r3的值的大小  28:   da000004    ble 40 <main+0x40> ;如果结果为小于则跳转到main+0x40的地址  2c:   e51b2008    ldr r2, [fp, #-8]  30:   e51b300c    ldr r3, [fp, #-12]  34:   e0633002    rsb r3, r3, r2  38:   e50b3010    str r3, [fp, #-16]  3c:   ea000003    b   50 <main+0x50> ;直接跳转到main+0x50地址  40:   e51b200c    ldr r2, [fp, #-12]  44:   e51b3008    ldr r3, [fp, #-8]  48:   e0633002    rsb r3, r3, r2  4c:   e50b3010    str r3, [fp, #-16]  50:   e1a00003    mov r0, r3  54:   e28bd000    add sp, fp, #0  58:   e8bd0800    pop {fp}           ;恢复fp指针  5c:   e12fff1e    bx  lr             ;返回倒link register 中的值

3. C 代码场景下的寄存器观察

(1)观察是否产生了寄存器移位寻址:

编写测试代码如下所示:

#include <stdio.h>  int main() {          int t[10];          int i = 0;          int j = 0;          for(j=0; j<4; j++) {                  t[8] = t[i*2];          }          printf("%d", t[8]);             return 0;  }  

汇编代码如下所示:

00000000 <main>:   0:   e92d4800    push    {fp, lr}   4:   e28db004    add fp, sp, #4   8:   e24dd030    sub sp, sp, #48 ; 0x30   c:   e3a03000    mov r3, #0  10:   e50b3008    str r3, [fp, #-8]  14:   ea00000b    b   48 <main+0x48>  18:   e51b3008    ldr r3, [fp, #-8]  1c:   e1a02083    lsl r2, r3, #1  20:   e3e0302b    mvn r3, #43 ; 0x2b  24:   e1a02102    lsl r2, r2, #2  28:   e24b1004    sub r1, fp, #4  2c:   e0812002    add r2, r1, r2  30:   e0823003    add r3, r2, r3  34:   e3a02001    mov r2, #1  38:   e5832000    str r2, [r3]  3c:   e51b3008    ldr r3, [fp, #-8]  40:   e2833001    add r3, r3, #1  44:   e50b3008    str r3, [fp, #-8]  48:   e51b3008    ldr r3, [fp, #-8]  4c:   e3530004    cmp r3, #4  50:   dafffff0    ble 18 <main+0x18>  54:   e51b3010    ldr r3, [fp, #-16]  58:   e59f0014    ldr r0, [pc, #20]   ; 74 <main+0x74>  5c:   e1a01003    mov r1, r3  60:   ebfffffe    bl  0 <printf>  64:   e3a03000    mov r3, #0  68:   e1a00003    mov r0, r3  6c:   e24bd004    sub sp, fp, #4  70:   e8bd8800    pop {fp, pc}  74:   00000000    .word   0x00000000

可以看到lsl指令,使用了寄存器移位,为后续进行寻址。

(2)观察一个复杂的 32 位数是如何装载到寄存器的:

编写测试代码:

#include <stdio.h>int main() {        unsigned int a = 0x12345678;        a++;        return 0;}

汇编代码结果如下所示,注释了相关32位数如何装在到寄存器中:

00000000 <main>:   0:   e52db004    push    {fp}        ; (str fp, [sp, #-4]!)   4:   e28db000    add fp, sp, #0   8:   e24dd00c    sub sp, sp, #12   c:   e59f3020    ldr r3, [pc, #32]   ; 34 <main+0x34>,取32位数读到r3中  10:   e50b3008    str r3, [fp, #-8]  14:   e51b3008    ldr r3, [fp, #-8]  18:   e2833001    add r3, r3, #1  1c:   e50b3008    str r3, [fp, #-8]  20:   e3a03000    mov r3, #0  24:   e1a00003    mov r0, r3  28:   e28bd000    add sp, fp, #0  2c:   e8bd0800    pop {fp}  30:   e12fff1e    bx  lr  34:   12345678    .word   0x12345678    ;以32位指令形式放置在pc代码段中

可以看到是使用存在代码段中的方法,然后用ldr来加载这个32位数。

4. ARM中的函数调用

下面我们要写一个 C 的多重函数调用的程序,然后我们观察和分析下面几个要点:    a. 调用时的返回地址在哪里?    b. 传入的参数在哪里?    c. 本地变量的堆栈分配是如何做的?    d. 寄存器是 caller 保存还是 callee 保存?是全体保存还是部分保存?

我们测试用的多重函数程序代码如下所示:

#include <stdio.h>int f1(int a,int b,int c,int d){        return f2(a,b,1,2)+f2(c,d,3,4);}int f2(int a, int b, int c, int d){        return f3(a,b,1)+f3(c,d,2);}int f3(int a,int b) {        int t = 2;        return a*b*t;}int main() {        int a,b,c;        a = 3;        b = 4;        c = 5;        d = 6;        printf("%d", f1(a,b,c));}

编译后反汇编.o文件查看结果,加上相关注释如下所示:

00000000 <f1>:   0:   e92d4810    push    {r4, fp, lr}   ;由于要用到r4所以先保护起来,同时保由于多级调用函数,要保护lr的值   4:   e28db008    add fp, sp, #8     8:   e24dd014    sub sp, sp, #20 ;申请栈地址空间   c:   e50b0010    str r0, [fp, #-16]  ;保护寄存器  10:   e50b1014    str r1, [fp, #-20]  14:   e50b2018    str r2, [fp, #-24]  18:   e50b301c    str r3, [fp, #-28]  1c:   e51b0010    ldr r0, [fp, #-16]  20:   e51b1014    ldr r1, [fp, #-20]  24:   e3a02001    mov r2, #1  28:   e3a03002    mov r3, #2  2c:   ebfffffe    bl  5c <f2>  ;r0r3传参数,调用函数f2  30:   e1a04000    mov r4, r0  ;保护返回值  34:   e51b0018    ldr r0, [fp, #-24]  38:   e51b101c    ldr r1, [fp, #-28]  3c:   e3a02003    mov r2, #3  40:   e3a03004    mov r3, #4  44:   ebfffffe    bl  5c <f2>  ;再次调用f2  48:   e1a03000    mov r3, r0  ;后续处理  4c:   e0843003    add r3, r4, r3  50:   e1a00003    mov r0, r3  54:   e24bd008    sub sp, fp, #8  58:   e8bd8810    pop {r4, fp, pc}    ;弹出保护的r4,fp以及用于返回函数的pc0000005c <f2>:  5c:   e92d4810    push    {r4, fp, lr}  60:   e28db008    add fp, sp, #8  64:   e24dd01c    sub sp, sp, #28  68:   e50b0010    str r0, [fp, #-16]  6c:   e50b1014    str r1, [fp, #-20]  70:   e50b2018    str r2, [fp, #-24]  74:   e50b301c    str r3, [fp, #-28]  78:   e3a03003    mov r3, #3  7c:   e58d3000    str r3, [sp]  80:   e51b0010    ldr r0, [fp, #-16]  84:   e51b1014    ldr r1, [fp, #-20]  88:   e3a02001    mov r2, #1  8c:   e3a03002    mov r3, #2  90:   ebfffffe    bl  c8 <f3>  ;此处传递参数用了sp指针来保存多于4个的参数  94:   e1a04000    mov r4, r0  98:   e3a03006    mov r3, #6  9c:   e58d3000    str r3, [sp]  a0:   e51b0018    ldr r0, [fp, #-24]  a4:   e51b101c    ldr r1, [fp, #-28]  a8:   e3a02002    mov r2, #2  ac:   e3a03004    mov r3, #4  b0:   ebfffffe    bl  c8 <f3>  b4:   e1a03000    mov r3, r0  b8:   e0843003    add r3, r4, r3  bc:   e1a00003    mov r0, r3  c0:   e24bd008    sub sp, fp, #8  c4:   e8bd8810    pop {r4, fp, pc}000000c8 <f3>:  ;与f1类似,只是取参数的时候会将sp(此处用fp来读)取出多余4个的参数  c8:   e52db004    push    {fp}        ; (str fp, [sp, #-4]!)  cc:   e28db000    add fp, sp, #0  d0:   e24dd01c    sub sp, sp, #28  d4:   e50b0010    str r0, [fp, #-16]  d8:   e50b1014    str r1, [fp, #-20]  dc:   e50b2018    str r2, [fp, #-24]  e0:   e50b301c    str r3, [fp, #-28]  e4:   e3a03002    mov r3, #2  e8:   e50b3008    str r3, [fp, #-8]  ec:   e51b3010    ldr r3, [fp, #-16]  f0:   e51b2014    ldr r2, [fp, #-20]  f4:   e0030392    mul r3, r2, r3  f8:   e51b2008    ldr r2, [fp, #-8]  fc:   e0020392    mul r2, r2, r3 100:   e51b301c    ldr r3, [fp, #-28] 104:   e59b1004    ldr r1, [fp, #4] 108:   e0030391    mul r3, r1, r3 10c:   e59b1008    ldr r1, [fp, #8] 110:   e0030391    mul r3, r1, r3 114:   e0823003    add r3, r2, r3 118:   e1a00003    mov r0, r3 11c:   e28bd000    add sp, fp, #0 120:   e8bd0800    pop {fp} 124:   e12fff1e    bx  lr00000128 <main>: 128:   e92d4800    push    {fp, lr} 12c:   e28db004    add fp, sp, #4 130:   e24dd010    sub sp, sp, #16 134:   e3a03003    mov r3, #3  ;初始化相关数值为3,4,5,6并保存到栈 138:   e50b3008    str r3, [fp, #-8] 13c:   e3a03004    mov r3, #4 140:   e50b300c    str r3, [fp, #-12] 144:   e3a03005    mov r3, #5 148:   e50b3010    str r3, [fp, #-16] 14c:   e3a03006    mov r3, #6 150:   e50b3014    str r3, [fp, #-20] 154:   e51b0008    ldr r0, [fp, #-8] 158:   e51b100c    ldr r1, [fp, #-12] 15c:   e51b2010    ldr r2, [fp, #-16] 160:   e51b3014    ldr r3, [fp, #-20] 164:   ebfffffe    bl  0 <f1>  ;取出栈中的值放入r0r3作为参数进行传递,调用f1 168:   e1a03000    mov r3, r0 16c:   e59f0010    ldr r0, [pc, #16]   ; 184 <main+0x5c>,取出当前需要输出的字符串的地址(实际为14c) 170:   e1a01003    mov r1, r3r1用于传递参数 174:   ebfffffe    bl  0 <printf>   ;调用printf函数 178:   e1a00003    mov r0, r3  ;保存返回值 17c:   e24bd004    sub sp, fp, #4 180:   e8bd8800    pop {fp, pc}    ;恢复帧指针和pc指针 184:   00000000    .word   0x00000000

通过上面的代码分析,值得引起我们注意的有:
a. 调用时的返回地址在lr寄存器当中,由于f1和f2当中用栈来保护了lr寄存器,返回的时候直接用pop到pc来直接解决了。
b. 传入的参数放在R0 R1 R2 R3四个寄存器中,多于4个参数的时候会将多出来的参数放在堆栈中传递
c. 本地变量存放在堆栈高地址,传进来的参数存放在堆栈低地址。
d. R0到R3由caller保存,R4以上由callee保存。

5. 编译特定指令

(1)MLA是带累加的乘法,尝试写 C 的表达式,使得能编译得到 MLA 指令。

MLA的指令格式是:

MLA   Rd,Rm,Rs,Rn

其将Rm和Rs中的值相乘,再将乘积加上第3个操作数,结果的最低32位保存到Rd中。那么我们可以设计下面的代码:

int f(int a, int b, int c){        return a*b+c;}int main(){        f(1,2,3);        return 0;}

编译后反汇编结果为(此时要加上-O1进行编译上的优化):

00000000 <f>:   0:   e0202091    mla r0, r1, r0, r2   4:   e12fff1e    bx  lr00000008 <main>:   8:   e3a00000    mov r0, #0   c:   e12fff1e    bx  lr

可以看到mla指令得到了使用,在优化中体现了其强大性

(2)BIC是对某一个比特清零的指令,尝试要如何写 C 的表达式能编译得到 BIC 指令。

BIC的指令格式为:

Rd,  Rn, Oprand2

BIC(位清除)指令对 Rn 中的值 和 Operand2 值的反码按位进行逻辑“与”运算。实现Bit Clear的功能。

那么我们可以设计代码如下

int f(int a,int b){        return a&~b;}int main(){        f(255,16);        return 0;}

编译后反汇编结果为:

00000000 <f>:   0:   e1c00001    bic r0, r0, r1   4:   e12fff1e    bx  lr00000008 <main>:   8:   e3a00000    mov r0, #0   c:   e12fff1e    bx  lr

看到BIC已经出现,通过特定的C表达获得了BIC指令。

6. 编写汇编函数实例

编写一个汇编函数,接受一个整数和一个指针做为输入,指针所指应为一个字符串,该汇编函数调用C语言的 printf()函数输出这个字符串的前n个字符,n即为那个整数。在C语言写的main()函数中调用并传递参数给这个汇编函数 来得到输出。

首先我们编写对应的汇编函数文件arm_print.asm,函数体如下所示:

.section .text.global arm_printarm_print:push {r2, r3, r4, fp, lr}#调用malloc函数申请空间并保护寄存器push {r0}add r0, r1, #1push {r1}bl mallocmov r2, r0pop {r1}pop {r0}#将字符串前n个字符复制到新的空间mov r3, #0loop1:ldrb r4, [r0,r3]strb r4, [r2,r3]add r3, r3, #1cmp r4, #0beq brcmp r3, r1beq brb loop1br:mov r4, #0strb r4, [r2,r3]#调用printf打印字符串push {r2}mov r0, r2bl printf;返回并释放空间pop {r2}mov r0,r2bl freepop {r2, r3, r4, fp, pc}

对应的,我们测试的c语言文件main.c,代码如下:

int main(){        const char * str = "testing";        int n = 2;        arm_print(str,n);}

随后我们先用下面命令分别对两个进行编译成.o文件:

arm-bcm2708hardfp-linux-gnueabi-as arm_print.asm -o arm_print.oarm-bcm2708hardfp-linux-gnueabi-gcc main.c -c

然后再将两个文件链接生成可执行文件:

arm-bcm2708hardfp-linux-gnueabi-gcc main.o arm_print.o -o test.out

使用scp命令发送到树莓派上进行检测:

scp test.out pi@192.168.1.106:/home/pi

此时用ssh登陆到树莓派上,运行文件,查看结果如下图所示:

结果

0 0