Model Me, Model World

Model Me, Model World

Blayground

ARM指令集初见

1. 实验板连接图

The picture is lost in the juggernaut of history

  1. Acadia 一块,远程 Linux 服务器一台,Windows 笔记本一台
  • 网线一根
  • 5V 直流电源
  • microUSB 线

2. 编译器调参

1. 生成了 Thumb 指令还是 ARM 指令

如何通过编译参数改变,相同的程序,ARM 和 Thumb 编译的结果有何不同,如指令本⾝和整体目标代码的大小等。

分别使用不同的编译选项编译 lzo 压缩算法代码,然后用objdump命令反汇编查看指令。
允许使用 Thumb 指令:

gcc -mtune=arm7tdmi -mthumb -o thumb.o lzo.c
objdump -d thumb.o

允许 Thumb 指令编译结果:

00008464 <_do_compress>:
    8464:	e92d 01f0 	stmdb	sp!, {r4, r5, r6, r7, r8}
    8468:	b08f      	sub	sp, #60	; 0x3c
    846a:	af00      	add	r7, sp, #0
    846c:	60f8      	str	r0, [r7, #12]
    846e:	60b9      	str	r1, [r7, #8]
    8470:	607a      	str	r2, [r7, #4]
    8472:	603b      	str	r3, [r7, #0]
    8474:	68fa      	ldr	r2, [r7, #12]
    8476:	68bb      	ldr	r3, [r7, #8]
    8478:	18d3      	adds	r3, r2, r3

只使用 ARM 指令:

gcc -mtune=arm7tdmi -marm -o arm.o lzo.c
objdump -d arm.o

只使用 ARM 指令编译结果:

00008464 <_do_compress>:
	8464:	e92d08f0 	push	{r4, r5, r6, r7, fp}
    8468:	e28db010 	add	fp, sp, #16
	846c:	e24dd03c 	sub	sp, sp, #60	; 0x3c
    8470:	e50b0040 	str	r0, [fp, #-64]	; 0x40
	8474:	e50b1044 	str	r1, [fp, #-68]	; 0x44
    8478:	e50b2048 	str	r2, [fp, #-72]	; 0x48
	847c:	e50b304c 	str	r3, [fp, #-76]	; 0x4c
    8480:	e51b2040 	ldr	r2, [fp, #-64]	; 0x40
	8484:	e51b3044 	ldr	r3, [fp, #-68]	; 0x44
	8488:	e0823003 	add	r3, r2, r3

(核心函数_do_compress部分编译结果)

对比:

a. 指令位数

Thumb 指令为 16bit 长,ARM 指令为 32bit 长。

b. 整体目标代码大小

允许 Thumb 指令时,_do_compress函数占用代码段0x8464~0x8937,共 1236 字节。
只使用 ARM 指令时,_do_compress函数占用代码段0x8464~0x8c14,共 1972 字节。
可见,使用了 Thumb 指令之后,目标代码整体大小更小。_do_compress函数的 Thumb 目标代码约比纯 ARM 目标代码小 37%。

c. 指令功能

Thumb 指令集不是一个完整的指令集,Thumb 指令只支持通用功能,在必要时需要借助于完善的 ARM 指令集。因此可以看到 Thumb 编译结果中含有 ARM 指令。
Thumb 指令在功能上受到一些限制。比如只能访问寄存器的一个子集,只有两个操作数,没有乘加指令及 64 位乘法指令,除了跳转指令 B 有条件执行功能外,其它指令均为无条件执行等。

ARM 指令功能完善,但生成二进制代码体积较大。

2. 对于 ARM 指令,能否产生条件执行的指令

指定使用 ARM 指令,并采用O1优化:

gcc -marm -O1 -o condition.o condition.c
objdump -d condition.o

源代码如下:

int fun(int a, int b){
    if(a>b) a++;
    if(a==b) a--;
    return a;
}

int main(void){
    fun(10,10);
    fun(2,3);
    return 0;
}

fun函数编译结果如下:

0000835c <fun>:
    835c:	e1500001 	cmp	r0, r1
    8360:	c2800001 	addgt	r0, r0, #1
    8364:	e1500001 	cmp	r0, r1
    8368:	02400001 	subeq	r0, r0, #1
    836c:	e12fff1e 	bx	lr

可以看到,编译结果产生了addgtsubeq条件执行指令。

3. 设计 C 的代码场景,观察是否产生了寄存器移位寻址

指定使用 ARM 指令,并采用O1优化:

gcc -marm -O1 -o shift.o shift.c
objdump -d shift.o

源代码如下:

int fun(int *a, int b){
    a[b*2] = 10;
    return a[b*4];
}

int main(void){
    int a[10];
    fun(a, 3);
    return 0;
}

fun函数编译结果如下:

0000835c <fun>:
    835c:	e3a0300a 	mov	r3, #10
    8360:	e7803181 	str	r3, [r0, r1, lsl #3]
    8364:	e7900201 	ldr	r0, [r0, r1, lsl #4]
    8368:	e12fff1e 	bx	lr

编译结果中str r3, [r0, r1, lsl #3]ldr r0, [r0, r1, lsl #4]使用了寄存器移位寻址。

4. 设计 C 的代码场景,观察⼀个复杂的 32 位数是如何装载到寄存器的

指定使用 ARM 指令,此时不使用O1优化:

gcc -marm -o load32.o load32.c
objdump -d load32.o

源代码如下:

int fun(int a, int b){
    return a+b;
}

int main(void){
    fun(10, 0x71234567);
    return 0;
}

main函数编译结果如下:

0000838c <main>:
    838c:	e92d4800 	push	{fp, lr}
    8390:	e28db004 	add	fp, sp, #4
    8394:	e3a0000a 	mov	r0, #10
    8398:	e3041567 	movw	r1, #17767	; 0x4567
    839c:	e3471123 	movt	r1, #28963	; 0x7123
    83a0:	ebffffed 	bl	835c <fun>
    83a4:	e3a03000 	mov	r3, #0
    83a8:	e1a00003 	mov	r0, r3
    83ac:	e8bd8800 	pop	{fp, pc}

当需要把一个 32 位数装载到寄存器时,先装载低 16 位,然后装载高 16 位。其中movt指令执行的动作包括使寄存器左移 16 位,然后做或操作。

5. 写一个 C 的多重函数调⽤的程序,观察和分析

指定使用 ARM 指令,此时不使用O1优化:

gcc -marm -o call.o call.c
objdump -d call.o

源代码如下:

int fun2(int a, int b, int c, int d, int e){
    return a+b+c+d+e;
}

int fun1(int a, int b){
    int c=11;
    int d=12;
    int e=13;
    return fun2(a, b, c, d, e);
}

int main(void){
    fun1(1, 2);
    return 0;
}

fun2函数编译结果如下:

0000835c <fun2>:
    835c:	e52db004 	push	{fp}		; (str fp, [sp, #-4]!)
    8360:	e28db000 	add	fp, sp, #0
    8364:	e24dd014 	sub	sp, sp, #20
    8368:	e50b0008 	str	r0, [fp, #-8]
    836c:	e50b100c 	str	r1, [fp, #-12]
    8370:	e50b2010 	str	r2, [fp, #-16]
    8374:	e50b3014 	str	r3, [fp, #-20]
    8378:	e51b2008 	ldr	r2, [fp, #-8]
    837c:	e51b300c 	ldr	r3, [fp, #-12]
    8380:	e0822003 	add	r2, r2, r3
    8384:	e51b3010 	ldr	r3, [fp, #-16]
    8388:	e0822003 	add	r2, r2, r3
    838c:	e51b3014 	ldr	r3, [fp, #-20]
    8390:	e0822003 	add	r2, r2, r3
    8394:	e59b3004 	ldr	r3, [fp, #4]
    8398:	e0823003 	add	r3, r2, r3
    839c:	e1a00003 	mov	r0, r3
    83a0:	e28bd000 	add	sp, fp, #0
    83a4:	e8bd0800 	pop	{fp}
    83a8:	e12fff1e 	bx	lr

fun1函数编译结果如下:

000083ac <fun1>:
    83ac:	e92d4800 	push	{fp, lr}
    83b0:	e28db004 	add	fp, sp, #4
    83b4:	e24dd020 	sub	sp, sp, #32
    83b8:	e50b0018 	str	r0, [fp, #-24]
    83bc:	e50b101c 	str	r1, [fp, #-28]
    83c0:	e3a0300b 	mov	r3, #11
    83c4:	e50b3010 	str	r3, [fp, #-16]
    83c8:	e3a0300c 	mov	r3, #12
    83cc:	e50b300c 	str	r3, [fp, #-12]
    83d0:	e3a0300d 	mov	r3, #13
    83d4:	e50b3008 	str	r3, [fp, #-8]
    83d8:	e51b3008 	ldr	r3, [fp, #-8]
    83dc:	e58d3000 	str	r3, [sp]
    83e0:	e51b0018 	ldr	r0, [fp, #-24]
    83e4:	e51b101c 	ldr	r1, [fp, #-28]
    83e8:	e51b2010 	ldr	r2, [fp, #-16]
    83ec:	e51b300c 	ldr	r3, [fp, #-12]
    83f0:	ebffffd9 	bl	835c <fun2>
    83f4:	e1a03000 	mov	r3, r0
    83f8:	e1a00003 	mov	r0, r3
    83fc:	e24bd004 	sub	sp, fp, #4
    8400:	e8bd8800 	pop	{fp, pc}

main函数编译结果如下:

00008404 <main>:
    8404:	e92d4800 	push	{fp, lr}
    8408:	e28db004 	add	fp, sp, #4
    840c:	e3a00001 	mov	r0, #1
    8410:	e3a01002 	mov	r1, #2
    8414:	ebffffe4 	bl	83ac <fun1>
    8418:	e3a03000 	mov	r3, #0
    841c:	e1a00003 	mov	r0, r3
    8420:	e8bd8800 	pop	{fp, pc}

a. 调用时的返回地址在哪?

返回地址存储在栈中。调用函数前,push {fp, lr}指令将帧指针寄存器和连接寄存器都压入了栈中。

b. 传递的参数在哪里?

当参数不多于 4 个时,参数通过r0r1r2r3寄存器传送。当参数多于 4 个时,超过的参数使用栈传送。
比如main函数中,调用fun1时传了两个参数,置于r0r1中:

    840c:	e3a00001 	mov	r0, #1
    8410:	e3a01002 	mov	r1, #2
    8414:	ebffffe4 	bl	83ac <fun1>

函数fun1中,调用fun2传了 5 个参数,其中一个置于栈中:

    83dc:	e58d3000 	str	r3, [sp]
    83e0:	e51b0018 	ldr	r0, [fp, #-24]
    83e4:	e51b101c 	ldr	r1, [fp, #-28]
    83e8:	e51b2010 	ldr	r2, [fp, #-16]
    83ec:	e51b300c 	ldr	r3, [fp, #-12]
    83f0:	ebffffd9 	bl	835c <fun2>

c. 本地变量的堆栈分配是如何做的?

本地变量是存在当前函数的中的,使用fp帧指针寄存器来管理。
比如fun1中的cde三个变量,分别存在fp-16fp-12fp-8的地址内。

83c0:	e3a0300b 	mov	r3, #11
83c4:	e50b3010 	str	r3, [fp, #-16]
83c8:	e3a0300c 	mov	r3, #12
83cc:	e50b300c 	str	r3, [fp, #-12]
83d0:	e3a0300d 	mov	r3, #13
83d4:	e50b3008 	str	r3, [fp, #-8]

d. 寄存器是 caller 保存还是 callee 保存?是全体保存还是部分保存?

callee 保存。部分保存,只保存需要用的寄存器原值。
比如fun1在设置了spfp寄存器后,把r0r1保存。

    83b8:	e50b0018 	str	r0, [fp, #-24]
    83bc:	e50b101c 	str	r1, [fp, #-28]

6. MLA 是带累加的乘法,尝试要如何写 C 的表达式能编译得到 MLA 指令

指定使用 ARM 指令,并采用O1优化:

gcc -marm -O1 -o mla.o mla.c
objdump -d mla.o

源代码如下:

int fun1(int a, int b, int c){
    return a*b+c;
}

int main(void){
    fun1(1, 2, 3);
    return 0;
}

fun1函数编译结果如下:

0000835c <fun1>:
    835c:	e0202091 	mla	r0, r1, r0, r2
    8360:	e12fff1e 	bx	lr

编译结果中mla r0, r1, r0, r2是 MLA 指令。

####7.BIC 是对某一个比特清零的指令,尝试要如何写 C 的表达式能编译得到 BIC 指令 指定使用 ARM 指令,此时不使用O1优化:

gcc -marm -o bic.o bic.c
objdump -d bic.o
int fun1(int a, int b, int c){
    return a&0xaaaa;
}

int main(void){
    fun1(1, 2, 3);
    return 0;
}
00008390 <fun1>:
    8390:	e3c00c55	bic	r0, r0, #21760  ; 0x5500
    8394:	e3c00055	bic	r0, r0, #85     ; 0x55
    8398:	e1a00800	lsl	r0, r0, #16
    839c:	e1a00820	lsr	r0, r0, #16
    83a0:	e12fff1e	bx	lr

Blog maintained by Tao J

No longer hosted on GitHub Pages — Theme by mattgraham