美文网首页
Lab2 : ARM指令 - 还是实验报告诶

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

作者: lmzqwer2 | 来源:发表于2016-03-31 14:45 被阅读493次
    连接图

    由于在实验一中已经配置好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<stdio.h>
    #include<time.h>
    
    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 
    
    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 <stdio.h>
    
    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 <main+60> 
       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 <main+64>
       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 <stdio.h>
    
    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 <stdio.h>
    
    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 <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);
        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 <fibo+52> ; 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 <fibo>
       0x00008454 <+32>:    cmn r4, #1  ; r4 -= 1, 即r4 = n - 2
       0x00008458 <+36>:    add r5, r5, r0
       0x0000845c <+40>:    bne 0x8448 <fibo+20> ; 尾递归优化
       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> ; 调用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 <stdio.h>
    
    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 <stdio.h>
    #include <string.h>
    
    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 <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;
    }
    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<a; i++)
            printf(“%c”, s[i]);
        return i;
    }
    

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

    root@Acadia:~/tmp/speed# cat 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;
    }
    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 <stdio.h>
    
    extern int add(int, int);
    
    int main(){
        int a, b, i, times;
        scanf("%d %d %d", &a, &b, &times);
        for (i=0; i<times; i++)
            a = add(a, b);
        printf("%d\n", a);
        return 0;
    }
    root@Acadia:~/tmp/ifspeed# cat testif.in
    1 2 20000000
    root@Acadia:~/tmp/ifspeed# gcc useif.S test.c -o useif -g -marm -O2
    ...
    root@Acadia:~/tmp/ifspeed# gcc noif.S test.c -o noif -g -marm -O2
    ...
    root@Acadia:~/tmp/ifspeed# time ./useif < testif.in 
    40000001
    
    real    0m0.254s
    user    0m0.240s
    sys 0m0.010s
    root@Acadia:~/tmp/ifspeed# time ./noif < testif.in 
    40000001
    
    real    0m0.259s
    user    0m0.240s
    sys 0m0.010s
    

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

    参考资料

    相关文章

      网友评论

          本文标题:Lab2 : ARM指令 - 还是实验报告诶

          本文链接:https://www.haomeiwen.com/subject/jhxdlttx.html