Lab2 : ARM指令 - 还是实验报告诶

连接图

由于在实验一中已经配置好Acadia的网络设置,所以这次直接插上网线,使用ssh进行远程登录。

实验目的

  1. 深入理解ARM指令和Thumb指令的区别和编译选项;
  2. 深入理解某些特殊的ARM指令,理解如何编写C代码来得到这些指令;
  3. 深入理解ARM的BL指令和C函数的堆栈保护;
  4. 深入理解如何实现C和汇编函数的互相调用。

实验步骤

1. arm与thumb指令集

经过查阅gcc的编译选项,找到了编译为arm的编译指令-marm以及编译为thumb的编译指令-mthumb。

使用程序验证如下,

root@Acadia:~/tmp/hello# cat hello.c 
#include
#include

int hello(){
    printf("Hello Acadia! At %lu\n", time(NULL));
}

int main(){
    hello();
    return 0;
}
root@Acadia:~/tmp/hello# gcc hello.c -marm -o arm.out
root@Acadia:~/tmp/hello# gcc hello.c -mthumb -o thumb.out
root@Acadia:~/tmp/hello# ll *.out
-rwxr-xr-x 1 root root 7856 Mar 26 10:44 arm.out*
-rwxr-xr-x 1 root root 7856 Mar 26 10:45 thumb.out*
root@Acadia:~/tmp/hello# echo disas hello | gdb arm.out > arm_hello.s
root@Acadia:~/tmp/hello# echo disas hello | gdb thumb.out > thumb_hello.s
root@Acadia:~/tmp/hello# vimdiff thumb_hello.s arm_hello.s 
Lab2 : ARM指令 - 还是实验报告诶_第1张图片
vimdiff两个文件的结果

首先,测试程序中调用了内部的函数hello,同时也调用了外部函数printf以及time。而通过gcc编译后,两个文件大小无差异,故不能从中直接获知是否为不同指令集的文件。

此时,需要使用gdb反汇编hello,进行比较后方可以看出生成程序的不同。测试命令中使用echo是为了命令的清晰,并且容易使用输出重定向比较结果。

程序之间最大的区别是在函数主体部分。主要区别有几点:

  1. thumb的指令地址一次只增加2,即每条指令只占16位。相对的,arm每个地址增加4,每条指令占位32位。
  2. thumb的指令集访问寄存器 r8 ~ r15 受到一定限制,所以在thumb程序中可以看出,使用的是r7寄存器代替了arm程序中的r11寄存器。
  3. thumb程序可以调用arm指令集的函数库,但是地址跳转需要使用blx进行指令集转换。而arm程序直接使用bl跳转即可。

2. arm上的条件命令

root@Acadia:~/tmp/if# cat if.c 
#include 

int main(){
    int n, m;
    scanf("%d", &m);
    if (m > 32){
        n = 5;
    }else{
        n = 1;
    }
    printf("%d\n", n);
    return 0;
}
root@Acadia:~/tmp/if# gcc if.c -marm -o if.out -O2
...
root@Acadia:~/tmp/if# echo disas main | gdb if.out > if_main.s
root@Acadia:~/tmp/if# cat if_main.s 
...
(gdb) Dump of assembler code for function main:
   0x00008374 <+0>: push    {lr}        ; (str lr, [sp, #-4]!)
   0x00008378 <+4>: sub sp, sp, #12
   0x0000837c <+8>: add r1, sp, #4
   0x00008380 <+12>:    ldr r0, [pc, #40]   ; 0x83b0  
   0x00008384 <+16>:    bl  0x835c <__isoc99_scanf>   ; r0 = "%d", r1 = &m, 调用scanf
   0x00008388 <+20>:    ldr r2, [sp, #4]
   0x0000838c <+24>:    mov r0, #1
   0x00008390 <+28>:    ldr r1, [pc, #28]   ; 0x83b4 
   0x00008394 <+32>:    cmp r2, #32  ; 执行if命令,使用cmp后条件赋值
   0x00008398 <+36>:    movle   r2, r0
   0x0000839c <+40>:    movgt   r2, #5
   0x000083a0 <+44>:    bl  0x8350 <__printf_chk> ; r2直接传入printf进行输出
   0x000083a4 <+48>:    mov r0, #0
   0x000083a8 <+52>:    add sp, sp, #12
   0x000083ac <+56>:    pop {pc}
   0x000083b0 <+60>:    andeq   r8, r0, r4, lsl #9
   0x000083b4 <+64>:    andeq   r8, r0, r8, lsl #9
End of assembler dump.
(gdb) quit

可以看出,if指令被编译成了3条指令,即cmp, movle, movgt分别用以条件赋值。其中,mov为基础指令,后方跟着的le表示小于等于,gt表示大于。

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

root@Acadia:~/tmp/shift# cat shift.c 
#include 

int main(){
    int a;
    scanf("%d", &a);
    a = a * 9;
    printf("%d\n", a);
    return 0;
}
root@Acadia:~/tmp/shift# gcc shift.c -marm -O2 -o shift.out
root@Acadia:~/tmp/shift# echo disas main | gdb shift.out > shift_main.s
root@Acadia:~/tmp/shift# cat shift_main.s 
...
(gdb) Dump of assembler code for function main:
   0x00008374 <+0>: push    {lr}        ; (str lr, [sp, #-4]!)
   0x00008378 <+4>: sub sp, sp, #12
   0x0000837c <+8>: add r1, sp, #4
   0x00008380 <+12>:    movw    r0, #33920  ; 0x8480
   0x00008384 <+16>:    movt    r0, #0
   0x00008388 <+20>:    bl  0x835c <__isoc99_scanf>
   0x0000838c <+24>:    ldr r2, [sp, #4]
   0x00008390 <+28>:    mov r0, #1
   0x00008394 <+32>:    movw    r1, #33924  ; 0x8484
   0x00008398 <+36>:    movt    r1, #0
   0x0000839c <+40>:    add r2, r2, r2, lsl #3    ; a = a * 9 = a << 3 + a
   0x000083a0 <+44>:    str r2, [sp, #4]
   0x000083a4 <+48>:    bl  0x8350 <__printf_chk>
   0x000083a8 <+52>:    mov r0, #0
   0x000083ac <+56>:    add sp, sp, #12
   0x000083b0 <+60>:    pop {pc}

由于9在二进制中表示为1001,所以使用移位加法的方式能够很好得避过消耗较大的乘法操作。此时add中使用的就是寄存器移位寻址功能。

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

root@Acadia:~/tmp/loadword# cat loadword.c 
#include 

int main(){
    int a = 2123456789;
    printf("%d\n", a);
    return 0;
}
root@Acadia:~/tmp/loadword# gcc loadword.c -O2 -marm -o loadword.out
root@Acadia:~/tmp/loadword# echo disas main | gdb loadword.out > loadword_main.s
root@Acadia:~/tmp/loadword# cat loadword_main.s 
...
(gdb) Dump of assembler code for function main:
   0x00008320 <+0>: push    {r3, lr}
   0x00008324 <+4>: mov r0, #1
   0x00008328 <+8>: movw    r1, #33808  ; 0x8410
   0x0000832c <+12>:    movw    r2, #24853  ; 0x6115 ; r2 = 0x00006115
   0x00008330 <+16>:    movt     r1, #0
   0x00008334 <+20>:    movt     r2, #32401 ; 0x7e91 ; r2 = 0x7e916115
   0x00008338 <+24>:    bl  0x8308 <__printf_chk>
   0x0000833c <+28>:    mov r0, #0
   0x00008340 <+32>:    pop {r3, pc}

使用计算器,可以求得2123456789 = 0x7E916115。所以,代码中的movw与movt分别将低位与高位载入寄存器中进行运算。而r1的movw主要是提供了printf的第一个操作数即格式字符串的位置。

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

  1. 调用时的返回地址在哪里?
  2. 传入的参数在哪里?
  3. 本地变量的堆栈分配是如何做的?
  4. 寄存器是 caller 保存还是 callee 保存?是全体保存还是部分保存?
root@Acadia:~/tmp/func# cat func.c 
#include 

int fibo(int n){
    if (n <= 1) return 1;
    return fibo(n-2) + fibo(n-1);
}

int main(){
    int a;
    scanf("%d", &a);
    a = fibo(a);
    printf("%d\n", a);
    return 0;
}
root@Acadia:~/tmp/func# vim func.c 
root@Acadia:~/tmp/func# gcc func.c -O2 -marm -o func.out
...
root@Acadia:~/tmp/func# echo disas main | gdb func.out > func_main.s
root@Acadia:~/tmp/func# echo disas fibo | gdb func.out > func_fibo.s
root@Acadia:~/tmp/func# cat func_fibo.s
...
(gdb) Dump of assembler code for function fibo:
   0x00008434 <+0>: cmp r0, #1
   0x00008438 <+4>: push    {r3, r4, r5, lr}
   0x0000843c <+8>: ble 0x8468  ; n <= 1 直接退出并返回1
   0x00008440 <+12>:    sub r4, r0, #2
   0x00008444 <+16>:    mov r5, #0
   0x00008448 <+20>:    mov r0, r4
   0x0000844c <+24>:    sub r4, r4, #1  ; r4 = r0 - 1 即 r4 = n - 1
   0x00008450 <+28>:    bl  0x8434 
   0x00008454 <+32>:    cmn r4, #1  ; r4 -= 1, 即r4 = n - 2
   0x00008458 <+36>:    add r5, r5, r0
   0x0000845c <+40>:    bne 0x8448  ; 尾递归优化
   0x00008460 <+44>:    add r0, r5, #1
   0x00008464 <+48>:    pop {r3, r4, r5, pc}
   0x00008468 <+52>:    mov r0, #1
   0x0000846c <+56>:    pop {r3, r4, r5, pc}
root@Acadia:~/tmp/func# cat func_main.s
...
(gdb) Dump of assembler code for function main:
   0x00008374 <+0>: push    {lr}        ; (str lr, [sp, #-4]!)
   0x00008378 <+4>: sub sp, sp, #12
   0x0000837c <+8>: add r1, sp, #4
   0x00008380 <+12>:    movw    r0, #33988  ; 0x84c4
   0x00008384 <+16>:    movt    r0, #0
   0x00008388 <+20>:    bl  0x835c <__isoc99_scanf>
   0x0000838c <+24>:    ldr r0, [sp, #4]
   0x00008390 <+28>:    bl  0x8434  ; 调用fibo函数
   0x00008394 <+32>:    movw    r1, #33992  ; 0x84c8
   0x00008398 <+36>:    movt    r1, #0
   0x0000839c <+40>:    mov r3, r0
   0x000083a0 <+44>:    mov r0, #1
   0x000083a4 <+48>:    mov r2, r3
   0x000083a8 <+52>:    str r3, [sp, #4]
   0x000083ac <+56>:    bl  0x8350 <__printf_chk>
   0x000083b0 <+60>:    mov r0, #0
   0x000083b4 <+64>:    add sp, sp, #12
   0x000083b8 <+68>:    pop {pc}
End of assembler dump.
(gdb) quit

a. 程序的返回地址在调用的时候默认存入lr寄存器,而在函数内为了保证返回结果的正确性,将其push进堆栈中。而后在函数结束的时候,将其pop至pc寄存器实现跳转返回。
b. 从fibo函数中看出,r0为函数的第一个参数。而在参数少于4个的时候,使用寄存器r0~r4传递参数。
c. 本地变量的堆栈分配使用push操作将原本的寄存器值存在堆栈中,当返回时再pop出来。而从 push {lr} === str lr, [sp, #-4]可以看出,堆栈是自顶向下伸展的。
d. 寄存器的值是collee保存。因为外部调用者不知道内部函数所需要的寄存器,保存也就无从谈起。
而函数内部用到的所有寄存器均会被保存,因为函数内部并不知道外部会使用哪些寄存器。

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

root@Acadia:~/tmp/mla# cat mla.c 
#include 

int main(){
    int a, tot;
    scanf("%d %d", &a, &tot);
    tot += a * a;
    printf("%d\n", tot);
    return 0;
}
root@Acadia:~/tmp/mla# gcc mla.c -marm -O2 -o mla.out
...
root@Acadia:~/tmp/mla# echo disas main | gdb mla.out > mla_main.s
root@Acadia:~/tmp/mla# cat mla_main.s 
...
(gdb) Dump of assembler code for function main:
   0x00008374 <+0>: push    {lr}        ; (str lr, [sp, #-4]!)
   0x00008378 <+4>: sub sp, sp, #12
   0x0000837c <+8>: add r2, sp, #4
   0x00008380 <+12>:    movw    r0, #33932  ; 0x848c
   0x00008384 <+16>:    mov r1, sp
   0x00008388 <+20>:    movt    r0, #0
   0x0000838c <+24>:    bl  0x835c <__isoc99_scanf>
   0x00008390 <+28>:    ldr r2, [sp, #4]
   0x00008394 <+32>:    ldr r3, [sp]
   0x00008398 <+36>:    mov r0, #1
   0x0000839c <+40>:    movw    r1, #33940  ; 0x8494
   0x000083a0 <+44>:    movt    r1, #0
   0x000083a4 <+48>:    mla r3, r3, r3, r2 ; tot += a * a => r3 = r3 * r3 + r2
   0x000083a8 <+52>:    mov r2, r3
   0x000083ac <+56>:    str r3, [sp, #4]
   0x000083b0 <+60>:    bl  0x8350 <__printf_chk>
   0x000083b4 <+64>:    mov r0, #0
   0x000083b8 <+68>:    add sp, sp, #12
   0x000083bc <+72>:    pop {pc}
End of assembler dump.
(gdb) quit

值得一提的是,在不使用-O2选项的情况下,编译的结果并不含mla。由此可见,代码优化的一种方式是将程序指令尽可能的贴合CPU,手动编写功能的运行效率较低。

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

root@Acadia:~/tmp/bic# cat bic.c 
#include 
#include 

int main(){
    int a, b;
    scanf("%d %d", &a, &b);
    a &= ~b;
    printf("%d\n", a);
    return 0;
}
root@Acadia:~/tmp/bic# gcc bic.c -marm -O2 -o bic.out
...
root@Acadia:~/tmp/bic# echo disas main | gdb bic.out > bic_main.s
root@Acadia:~/tmp/bic# cat bic_main.s 
...
(gdb) Dump of assembler code for function main:
   0x00008374 <+0>: push    {lr}        ; (str lr, [sp, #-4]!)
   0x00008378 <+4>: sub sp, sp, #12
   0x0000837c <+8>: add r2, sp, #4
   0x00008380 <+12>:    movw    r0, #33928  ; 0x8488
   0x00008384 <+16>:    mov r1, sp
   0x00008388 <+20>:    movt    r0, #0
   0x0000838c <+24>:    bl  0x835c <__isoc99_scanf>
   0x00008390 <+28>:    ldr r3, [sp]
   0x00008394 <+32>:    ldr r2, [sp, #4]
   0x00008398 <+36>:    mov r0, #1
   0x0000839c <+40>:    movw    r1, #33936  ; 0x8490
   0x000083a0 <+44>:    movt    r1, #0
   0x000083a4 <+48>:    bic r2, r3, r2 ; a &= ~b
   0x000083a8 <+52>:    str r2, [sp]
   0x000083ac <+56>:    bl  0x8350 <__printf_chk>
   0x000083b0 <+60>:    mov r0, #0
   0x000083b4 <+64>:    add sp, sp, #12
   0x000083b8 <+68>:    pop {pc}
End of assembler dump.
(gdb) quit

bic指令将某些由标示数指定的bit清零,其原理是通过将标示数进行取反后取and,即所有原本在标示数中为1的位,经过取反之后为0。通过and操作,将所有的0位覆盖至目标,同时却又不覆盖其他bit的值。

8. 编写一个汇编函数。

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

root@Acadia:~/tmp/asm# cat cutprint.S 
.global cutprint
cutprint:
    push {R5, R6, R7, lr}
    MOV R5, R0
    MOV R6, R1
    MOV R7, #0 ; 首先,将r0, r1保存起来,同时将计数器r7置零
    CMP R7, R6
    BGE exit  ; 如果r7大于r6的话,直接返回,因为r6必定是负数
begin:
    LDR R0, =char ; 由于函数返回值在r0的位置,所以每次调用玩函数,"%c"就会被覆盖,需要再载入一次
    LDR R1, [R5, R7] ; 根据字符串以及r7计数器的下标,载入字符
    CMP R1, #0 ; 如果遇到了c风格字符串结尾'\0',直接跳出循环
    BEQ exit
    bl printf
    ADD R7, R7, #1 ; 计数器+1
    CMP R7, R6 
    BLT begin ;判断是否循环结束,未结束则跳到begin进行下一轮循环
exit:
    LDR R0, =newline ; 输出结束换行
    bl printf
    MOV R0, R7 ; 把输出的计数器作为返回值
    pop {R5, R6, R7, pc}

.data
    char: .asciz "%c"
    newline: .asciz "\n"
root@Acadia:~/tmp/asm# cat cutprint.c 
#include 

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;
}
root@Acadia:~/tmp/asm# gcc cutprint.c cutprint.S -o cutprint -g -marm
root@Acadia:~/tmp/asm# ./cutprint
123456789 -1

Print 0 character.
root@Acadia:~/tmp/asm# ./cutprint
123456789 5
12345
Print 5 character.
root@Acadia:~/tmp/asm# ./cutprint
123456789 19999
123456789
Print 9 character.

其中,cutpinrt.s的程序逻辑类似如下:

int cutprint(cahr *s, int a){
    int i = 0;
    for (i; i

9. 编写测试程序,测试ARM指令和Thumb指令的执行效率

root@Acadia:~/tmp/speed# cat speed.c 
#include 

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;
}
root@Acadia:~/tmp/speed# gcc speed.c -marm -o arm.out
root@Acadia:~/tmp/speed# gcc speed.c -mthumb -o thumb.out
root@Acadia:~/tmp/speed# echo 40 > speed.in
root@Acadia:~/tmp/speed# time ./arm.out < speed.in
165580141

real    0m5.951s
user    0m5.940s
sys 0m0.000s
root@Acadia:~/tmp/speed# time ./thumb.out < speed.in
165580141

real    0m6.425s
user    0m6.400s
sys 0m0.010s
root@Acadia:~/tmp/speed# gcc speed.c -marm -o arm.out -O2
...
root@Acadia:~/tmp/speed# gcc speed.c -mthumb -o thumb.out -O2
...
root@Acadia:~/tmp/speed# time ./arm.out < speed.in
165580141

real    0m2.190s
user    0m2.180s
sys 0m0.000s
root@Acadia:~/tmp/speed# time ./thumb.out < speed.in
165580141

real    0m3.988s
user    0m3.990s
sys 0m0.000s
root@Acadia:~/tmp/speed# gcc speed.c -marm -o arm.out -O3
...
root@Acadia:~/tmp/speed# gcc speed.c -mthumb -o thumb.out -O3
...
root@Acadia:~/tmp/speed# time ./arm.out < speed.in
165580141

real    0m2.520s
user    0m2.510s
sys 0m0.000s
root@Acadia:~/tmp/speed# time ./thumb.out < speed.in
165580141

real    0m2.723s
user    0m2.710s
sys 0m0.010s

程序中,主要消耗时间的地方在于fibo(40)的递归调用,可以看出,thumb整体而言还是偏慢的,但是如果开了编译优化之后,差别不会过大。

10. 编写测试程序,测试使用带条件的ARM指令和不使用时的执行效率。

root@Acadia:~/tmp/ifspeed# cat noif.S
.global add
add:
    CMP R0, #0
    ADD R0, R0, R1
    MOV pc, lr
root@Acadia:~/tmp/ifspeed# cat useif.S
.global add
add:
    CMP R0, #0
    ADDNE R0, R0, R1
    MOV pc, lr
root@Acadia:~/tmp/ifspeed# cat test.c
#include 

extern int add(int, int);

int main(){
    int a, b, i, times;
    scanf("%d %d %d", &a, &b, ×);
    for (i=0; i

经过多次比对,虽然时间略有波动。但是总体而言,两程序执行时间基本没有区别,即ADDNE与ADD的执行基本没有时间差。

参考资料

  • ARM指令和Thumb指令的区别
  • GCC的ARM编译选项
  • ARM指令如何在thumb和arm模式切换
  • ARM指令集详解
  • arm c函数的调用过程arm汇编语言调用C函数之参数传递
  • Hello World in ARM Assembly Language

你可能感兴趣的:(Lab2 : ARM指令 - 还是实验报告诶)