【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要比较小:
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 ;比较r2和r3的值的大小 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> ;r0~r3传参数,调用函数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> ;取出栈中的值放入r0~r3作为参数进行传递,调用f1 168: e1a03000 mov r3, r0 16c: e59f0010 ldr r0, [pc, #16] ; 184 <main+0x5c>,取出当前需要输出的字符串的地址(实际为14c) 170: e1a01003 mov r1, r3 ;r1用于传递参数 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登陆到树莓派上,运行文件,查看结果如下图所示:
- 【Raspberry Pi系列】4. ARM指令
- 在Raspberry Pi上搭建ARM Cortex-M3开发环境
- ARM assembler in Raspberry Pi – Chapter 1
- ARM assembler in Raspberry Pi – Chapter 3
- OPENWRT ON ARM-BASED PLATFORM (RASPBERRY PI 2)
- Raspberry PI 系列 —— 裸机点亮LED灯
- 【Raspberry Pi系列】1. 启动与进入树莓派
- 【Raspberry Pi系列】2. 多种传输方式的比较
- 【Raspberry Pi系列】3. 交叉编译环境配置
- 【Raspberry Pi入门系列2】Raspbian/Linux终端常用命令
- Archlinux arm的国内镜像源(for Banana Pi / Raspberry Pi , etc)
- Raspberry Pi: SSH连接Raspberry Pi
- Raspberry Pi 定制Archlinux
- Raspberry Pi安装OpenELEC
- 使用 Raspberry Pi 远程桌面
- MK808 vs Raspberry Pi
- 开始折腾 Raspberry Pi
- raspberry pi 镜像烧写
- Redis与Memcached的区别
- 全排列
- 欧拉路 (Fleury算法)
- Fortran 循环
- 第五周项目2(1)
- 【Raspberry Pi系列】4. ARM指令
- 第五周上机实践项目 项目2--游戏中的角色类(2)
- 腾讯在线笔试题-把字符串“I am from china.”反转成为“I am from china.”,以及把整个字符串逆序。
- 第五周项目2(2)
- 第6周项目1—分数类的雏形
- Form token KO80SIJW4F84034NG5HM1ZBUGOVNY64D does not match the session token null.
- Leetcode 95. Unique Binary Search Trees II
- Nmap的那些事儿
- 最长上升子序列