这个实验的目的是深入理解ARM和Thumb指令的特点,了解编译选项对代码产生的影响。
配合课程
ARM处理器
实验目的
- 深入理解ARM指令和Thumb指令的区别和编译选项;
- 深入理解某些特殊的ARM指令,理解如何编写C代码来得到这些指令;
- 深入理解ARM的BL指令和C函数的堆栈保护;
- 深入理解如何实现C和汇编函数的互相调用。
实验器材
硬件
- 实验主板一块(raspberryPi);
- 5V/1A电源一个;
- microUSB线一根;
软件
- 交叉编译软件(arm-linux-gcc)
操作方法和实验步骤
-
实验设备连接示意图
-
同一个程序,用arm指令和thumb指令编译,得到的结果有什么不同
代码如下:
//hello.c
#include <stdio.h>int main(){ printf("Hello World\n"); return 0; }
用两种不同的方式编译:
得到的结果:
区别:
- thumb的指令地址一次只增加2,即每条指令只占16位。相对的,arm每个地址增加4,每条指令占位32位。
- 在 Thumb 状态下,单寄存器加载和存储指令只能访问寄存器 R0~R7。
-
对于 ARM 指令,能否产生条件执行的指令
可以的,C语言代码:
//if.c
#include <stdio.h>
int main(){
int i;
i = 1;
if(i == 1)i++;
else i--;
return 0;
}
生成的ARM指令代码<main>函数部分:
其中可以看到bne指令。 -
设计 C 的代码场景,观察是否产生了寄存器移位寻址
C语言代码如下:
//shift.c
#include <stdio.h>
int lShift(int *i, int j){
return i[j << 2];
}
int main(){
int i[256];
int j = 18;
lShift(i, j);
return 0;
}
生成的ARM指令代码主要部分(可以看到ldr指令):
-
设计 C 的代码场景,观察一个复杂的 32 位数是如何装载到寄存器的
C语言代码如下:
//load32.c
#include <stdio.h>
int main(){
double i;
long j;
i = 204800001.1;
j = 2048000011;
return 0;
}
生成的ARM代码如下:
可以看出,ARM是将32位数放到堆栈中,然后依次取出放入到寄存器当中。 -
写一个 C 的多重函数调用的程序,观察和分析: a. 调用时的返回地址在哪里? b. 传入的参数在哪里? c. 本地变量的堆栈分配是如何做的? d. 寄存器是 caller 保存还是 callee 保存?是全体保存还是部分保存?
C代码如下:
//mulFunc.c
#include <stdio.h>
int fun1(int i){
int j = i + 1;
return j;
}int fun2(int i, int j){ int x = i + j; x = fun1(x); return x; } int main(){ int a = 1, b = 2; a = fun2(a, b); return 0; }
得到的ARM汇编代码如下:
a.调用时的返回地址放在lr寄存器当中。
b.传递的参数放在了堆栈中。
c.被调函数首先将传递过来的堆栈指针 sp 拷贝到指针 fp 中,然后将本地变量从 r0 开始存放,当存放到 r2 之后,其余的变量将会通过 r3 存放到堆栈中去,最后将 fp 中保存的指针拷贝回 sp,恢复被调前的堆栈情况,然后返回。
d.参数保存在caller,地址保存在callee,部分保存的
-
MLA 是带累加的乘法,尝试要如何写 C 的表达式能编译得到 MLA 指令
C代码如下:
//mla.c
#include <stdio.h>
int mla(int i, int j, int k){
return i * j + k;
}int main(){ int i = 2, j = 20, k = 30; mla(i, j, k); return 0; }
编译得到的ARM汇编文件(编译时用-O1优化编译)
可以看到mla指令。
-
BIC是对某一个比特清零的指令,尝试要如何写 C 的表达式能编译得到 BIC 指令
C代码如下:
//bic.c
#include <stdio.h>
int bic(int i, int j){
return i&~j;
}int main(){ int i = 0x8828; int j = 20484; bic(i, j); return 0; }
汇编得到的ARM指令(要使用-O1编译优化)
可以很明显的看到bic指令。
-
编写一个汇编函数,接受一个整数和一个指针做为输入,指针所指应为一个字符串,该汇编函数调用C语言的 printf()函数输出这个字符串的前n个字符,n即为那个整数。在C语言写的main()函数中调用并传递参数给这个汇编函数来得到输出。
汇编函数代码(cutprint.S):
.global cutprint
cutprint:
push {R5, R6, R7, lr}
MOV R5, R0
MOV R6, R1
MOV R7, #0
CMP R7, R6
BGE exit
begin:
LDR R0, =char
LDR R1, [R5, R7]
CMP R1, #0
BEQ exit
bl printf
ADD R7, R7, #1
CMP R7, R6
BLT begin
exit:
LDR R0, =newline
bl printf
MOV R0, R7
pop {R5, R6, R7, pc}.data char: .asciz "%c" newline: .asciz "\n"
C语言主函数代码:
//cutprint.c
#include <stdio.h>
extern int cutprint(char *, int);
int main(){
int a;
char s[100];
scanf("%s %d", s, &a);
a = cutprint(s, a);
printf("Print %d character.\n", a);
return 0;
}
编译后在树莓派的执行结果:
-
自选扩展内容1:编写测试程序,测试ARM指令和Thumb指令的执行效率
测试代码:
//speed.c
#include <stdio.h>int fibo(int n){ if(n <= 1) return 1; return fibo(n - 2) + fibo(n - 1); } int main(){ int a; scanf("%d", &a); printf("%d\n", fibo(a)); return 0; }
在编译时,在普通编译之外,还有-O2和-O3编译优化的编译。
从测试结果中可以看到,arm的执行速度要比thumb的执行速度快一些。
-
自选扩展内容2:编写测试程序,测试使用带条件的ARM指令和不使用时的执行效率。
含带条件的ARM指令(useif.S):
.global add
add:
CMP R0, #0
ADDNE R0, R0, R1
MOV pc, lr
不含的ARM指令(noif.S):.global add add: CMP R0, #0 ADD R0, R0, R1 MOV pc, lr
测试代码:
//test.c
#include <stdio.h>
extern int add(int, int);
int main(){
int a, b, i, times;
scanf("%d %d %d", &a, &b, ×);
for (i=0; i<times; i++)
a = add(a, b);
printf("%d\n", a);
return 0;
}
编译选项:
通过比较执行结果可以发现两个程序执行的时间基本上没有什么差别。
讨论与心得
本次实验主要是对代码的编写和对ARM指令的了解,没有涉及过多的硬件上的内容。需要的硬件内容(交叉编译环境以及连接和文件传递)在上次实验已经设计,所以从总体上来说比上一次实验简单。
同时本次使用和让我了解到了ARM指令的一些性质,以及ARM指令和Thumb指令的区别。