zoukankan      html  css  js  c++  java
  • armv8 汇编入门

    准备环境

    • aarch64-linux-gnu-gcc: 可以通过下载 linaro 交叉编译工具链获得
    • qemu-system-aarch64
    • aarch64-linux-gnu-gdb: 可以通过下载 linaro 交叉编译工具链获得

    一个简单的汇编程序

    首先,创建一个空目录,例如,名为aarch64_assembly。然后,创建一个名为entry.S的汇编程序,其内容为:

    #define BEGIN_FUNC(name) 
    	.global name; 
    	.type name, %function; 
    	name:
    
    #define END_FUNC(name) 
    	.size name, . - name
    
    BEGIN_FUNC(_start)
    	.align 7
    	nop
    	
    	mov x0, 0x1
    	mov x1, 0x2
    	add x0, x0, x1
    
    loop:
    	b loop
    END_FUNC(_start)
    

    这里暂时不管BEGIN_FUNCEND_FUNC宏,就当它是用来定义函数的。_start是一个默认的函数名,就像C语言中的main

    然后我们写个简单的Makefile进行构建,避免每次都要输入一长串的内容进行编译。其内容如下:

    CC = aarch64-linux-gnu-gcc
    LD = aarch64-linux-gnu-ld
    
    CFLAGS = -g -O0 -nostdlib -nodefaultlibs
    
    simple_prog: entry.o
    	$(LD) -o $@ $^
    
    %.o: %.S
    	$(CC) $(CFLAGS) -c $< -o $@
    
    .PHONY: clean
    
    clean:
    	-rm entry.o
    	-rm simple_prog
    

    OK,现在我们可以进行运行调试了。因为还没有打印输出,所以我们暂时通过qemu内的gdb server进行调试。同样地,为了避免重复输入,搞一个run.sh脚本。

    qemu-system-aarch64 
    	-machine virt,virtualization=true,gic-version=3 
    	-nographic 
    	-m size=1024M 
    	-s 
     	-kernel ./simpe_prog 
    	-cpu cortex-a57 -smp 1 
    	$@
    

    现在就可以调试了。首先在当前界面输入./run.sh -S,这个时候qemu会卡住,等待gdb的输入。

    然后新开一个shell窗口,输入如下指令

    root@ubuntu:~/xxx# aarch64-linux-gnu-gdb ./simple_prog
    (gdb) target remote localhost:1234
    (gdb) ...
    

    好了,可以愉快地使用gdb进行调试了。特别提醒,si单步执行每条指令,print $reg打印寄存器内容。调试结束之后,可以Ctrl+a,然后x结束qemu执行。

    函数调用

    我们知道,对于ARM来说,CPU根据PC寄存器中的地址值取指令,然后进行译码和执行。当我们通过bl指令进行函数调用的时候,显然,PC寄存器中的地址值会变为跳转地址。此外,lr(即x30)中会保存bl指令后下一条指令的地址。

    我们实际通过程序来看一下。将加法功能抽出来单独成为一个函数,即修改entry.S

    #define BEGIN_FUNC(name) 
    	.global name; 
    	.type name, %function; 
    	name:
    
    #define END_FUNC(name) 
    	.size name, . - name
    
    BEGIN_FUNC(add)
    	add x0, x0, x1
    	ret
    
    END_FUNC(add)
    
    BEGIN_FUNC(_start)
    	.align 7
    	nop
    
    	mov x0, 0x1
    	mov x1, 0x2
    	bl add
    
    loop:
    	b loop
    
    END_FUNC(_start)
    

    类似前一节的方法进行调试,在bl指令和ret指令前后,注意看lr寄存器值的变化。

    调用C函数

    显然, 用汇编开发的效率是极低的。因此,在一些C语言也可能解决问题且对性能没有极高要求的场景,我们可以通过C语言进行编写。通过汇编调用C语言函数需要关注ABI(即 Application Binary Interface),C语言调用汇编可以通过内联汇编(Inline Assembly)。我们首先关注ABI

    ABI主要用于制定一致的规则,从而让各个可执行的代码模块之间可以相互调用。其内容包括ELF(Executable and Linkable Format)标准、PCS(Procedure Call Standard)标准、DWARF(Debugging With Attributed Record Formats)标准等。

    这里我们主要关注PCS。每个函数都会有自己的栈帧,栈帧中保存着传入的参数、函数内部声明的局部变量、传给其他函数的参数等。其一般结构如下图所示:

    其中,传入的参数从寄存器或者Stack args area区域获得,和函数内部声明的局部变量一起,存放于Local variables区域。

    aarch64中,可以基于函数调用将通用寄存器分为四组:

    1. 参数寄存器(X0X7
      这些寄存器用于传递参数和保存返回值。当然,对于结构体这种空间较大的参数显然它们也是保存不了的。
    2. 调用者保存的寄存器(x9x15
    3. 被调用者保存的寄存器(x19x29
    4. 有特殊目的的寄存器(x8x16x18x29x30
      其中,x8是间接结果寄存器。它被用来保存一个间接结果的地址,比如,当函数返回一个很大的结构体时。x30是链接寄存器,用来保存函数的返回地址。

    首先,我们创建一个main.c,里面定义一个sum求和函数。其内容如下:

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

    这个函数参数能够通过一个通用寄存器传递,因此是通过寄存器x0 ~ x7传递的。返回值也能够通过通用寄存器传递,因此是通过x0返回的。因此,我们只需要修改entry.S,去掉其中的sum函数,即其内容改为

    #define BEGIN_FUNC(name) 
    	.global name; 
    	.type name, %function; 
    	name:
    
    #define END_FUNC(name) 
    	.size name, . - name
    
    BEGIN_FUNC(_start)
    	.align 7
    	nop
    
    	adr x0, stack_top
    	mov sp, x0
    	mov x0, 0x1
    	mov x1, 0x2
    	bl add
    
    loop:
    	b loop
    
    END_FUNC(_start)
    

    在函数调用的过程中,我们需要调用栈空间。我们通过链接脚本创建,即创建simple_prog.lds内容如下:

    OUTPUT_FORMAT("elf64-littleaarch64", "elf64-bigaarch64", "elf64-littleaarch64")
    OUTPUT_ARCH(aarch64)
    
    ENTRY(_start)
    SECTIONS
    {
    	. = 0x40000000;
    	.startup . : { entry.o(.text) }
    	.text : { *(.text) }
    	.data : { *(.data) }
    	.bss : { *(.bss COMMON) }
    	. = ALIGN(8);
    	. = . + 0x1000; /* 4kB of stack memory */
    	stack_top = .;
    }
    

    因为添加了新的.c文件和链接脚本,所以我们需要同时修改Makefile内容如下:

    CC = aarch64-linux-gnu-gcc
    LD = aarch64-linux-gnu-ld
    
    CFLAGS = -g -O0 -nostdlib -nodefaultlibs
    LDFLAGS := -static  -L ./  -T ./simple_prog.lds
    
    simple_prog: entry.o main.o
    	$(LD) $(LDFLAGS) -o $@ $^
    
    %.o: %.S
    	$(CC) $(CFLAGS) -c $< -o $@
    
    %.o: %.c
    	$(CC) $(CFLAGS) -c $< -o $@
    
    .PHONY: clean
    
    clean:
    	-rm entry.o
    	-rm main.o
    	-rm simple_prog
    

    OK,编译链接,然后调试。

    那对于比较大的,通用寄存器放不下的结构体之类的参数是如何传递的呢?首先,如果结构体的大小小于等于16字节,那么会尝试通过通用寄存器传递。如果结构体太大了,那么就会尝试通过通用寄存器传递结构体的基地址。看个简单的例子,修改main.c

    #define MAX 10
    
    struct score {
    	int arr[MAX];
    };
    
    struct score score_add(int delta, int which, struct score s_id)
    {
    	if (which >= 0 && which < MAX) {
    		s_id.arr[which] += delta;
    	}
    
    	return s_id;
    }
    

    然后修改entry.S调用该函数

    #define BEGIN_FUNC(name) 
    	.global name; 
    	.type name, %function; 
    	name:
    
    #define END_FUNC(name) 
    	.size name, . - name
    
    BEGIN_FUNC(_start)
    	.align 7
    	nop
    
    	adr x0, stack_top
    	mov sp, x0
    	sub sp, sp, 0x28   # store structure score
    	mov x0, 0x50
    	str x0, [sp]
    	mov x2, sp       # parameter s_id
    	mov x8, sp	 # return value base address
    	mov x0, 0x5      # delta = 5
    	mov x1, 0x0      # which = 0
    	bl score_add
    
    loop:
    	b loop
    
    END_FUNC(_start)
    

    编译调试之后,我们可以看到这样的汇编指令:

    40000030:       str     x19, [sp, #-32]!
    40000034:       mov     x3, x8
    40000038:       str     w0, [sp, #28]      # delta
    4000003c:       str     w1, [sp, #24]      # which
    40000040:       mov     x19, x2            # s_id
    40000044:       ldr     w0, [sp, #24]
    40000048:       cmp     w0, #0x0
    4000004c:       b.lt    40000074 <score_add+0x44>  // b.tstop
    40000050:       ldr     w0, [sp, #24]
    40000054:       cmp     w0, #0x9
    40000058:       b.gt    40000074 <score_add+0x44>
    4000005c:       ldrsw   x0, [sp, #24]
    40000060:       ldr     w1, [x19, x0, lsl #2]
    40000064:       ldr     w0, [sp, #28]
    40000068:       add     w1, w1, w0
    4000006c:       ldrsw   x0, [sp, #24]
    40000070:       str     w1, [x19, x0, lsl #2]
    40000074:       mov     x0, x3
    40000078:       mov     x1, x19
    4000007c:       ldp     x2, x3, [x1]
    40000080:       stp     x2, x3, [x0]
    40000084:       ldp     x2, x3, [x1, #16]
    40000088:       stp     x2, x3, [x0, #16]
    4000008c:       ldr     x1, [x1, #32]
    40000090:       str     x1, [x0, #32]
    40000094:       ldr     x19, [sp], #32
    40000098:       ret
    

    至于当通用寄存器个数不够的情况下,参数是如何传递的,大家可以自行查看相关手册标准并试验之。

    内联汇编

    有时候,我们在C代码中需要使用汇编代码,比如我们想知道当前用的是SP_EL0还是SP_ELx,显然是没有C语句能直接查询到的,这时我们可以使用内联汇编。我们添加一个新的函数check_el,检查当前的EL级别,即修改main.c内容如下:

    #define MAX 10
    
    struct score {
    	int arr[MAX];
    };
    
    struct score score_add(int delta, int which, struct score s_id)
    {
    	if (which >= 0 && which < MAX) {
    		s_id.arr[which] += delta;
    	}
    
    	return s_id;
    }
    
    void check_el(void)
    {
    	int a;
    
    	asm volatile("mrs x0, CurrentEL
    	"
    				 "lsr x0, x0, #2
    	"
    				 "str x0, %[result]
    	"
    				 : [result]"=m" (a)
    				 :
    				 : "x0");
    }
    

    并修改entry.S以调用它:

    #define BEGIN_FUNC(name) 
    	.global name; 
    	.type name, %function; 
    	name:
    
    #define END_FUNC(name) 
    	.size name, . - name
    
    BEGIN_FUNC(_start)
    	.align 7
    	nop
    
    	adr x0, stack_top
    	mov sp, x0
    	sub sp, sp, 0x28   # store structure score
    	mov x0, 0x50
    	str x0, [sp]
    	mov x2, sp       # parameter s_id
    	mov x8, sp		 # return value base address
    	mov x0, 0x5      # delta = 5
    	mov x1, 0x0      # which = 0
    	bl score_add
    	bl check_el
    
    loop:
    	b loop
    
    END_FUNC(_start)
    

    然后可以编译调试。这里我们提一嘴内联汇编的格式。其通用格式为

    asm(code : output operand list : input operand list : clobber list);
    

    也就是说,各个部分是通过冒号分隔的。我们使用内联汇编的时候,总是不可避免的需要将变量牵扯进去,就像上文中check_el中的变量a。这里我们通过符号名result对该变量进行引用。即通过[result] "=m" (a)将符号名和变量a关联起来,并在代码部分通过%[result]引用该变量。(不过在GCC 3.1之前是通过数字编号引用变量的。也就是说,它通过"=m" (a)建立变量和汇编代码的关联,通过%0这种类似的数字编号引用变量。)

    那么[result] "=m" (a)中的=m是什么意思呢?我们在汇编指令中一般使用三种不同类型操作数:寄存器、内存地址、立即数。例如,mov操作的第一个操作数必然是寄存器,而str操作的第二个操作数必然是内存地址。这里的m表明我们使用变量的内存地址,其他的类型还有rI等。m前面的=号表明这个变量是可写的,但是不可读。其他可选的符号还有+&

    就像其他被调用的函数,我们要避免破坏调用函数的上下文,所以在使用x19这样的寄存器时需要先将其最初的值压入栈中。上文的内联汇编中,在clobber list中我们表明使用了x0寄存器,希望gcc在使用前保存一下, 防止破坏了函数上下文。

    最后volatile关键字是为了防止编译器做代码优化,就像我们这里,变量a是一个局部变量,也没有返回,那么gcc可能就直接把相关语句优化掉了。

    参考资料

    本文来自博客园,作者:Legend_Lone,转载请注明原文链接:https://www.cnblogs.com/sun-ye/p/14992167.html

  • 相关阅读:
    【WPF】操作RichTextBox(取值、赋值、清空、滚动条自动滚动实例、文本自动滚动实例)
    系统初始化 服务列表
    多个filter如何决定调用顺序
    IE浏览器 查看Form对象
    java try_catch 分析
    关于ClassLoader 和Class的俩个记录
    lis分析之一一批处理(任务)如何连接数据库的
    document.all("div).style.display = "none"与 等于""的区别
    Mybatis Util包
    Spring创建bean对象的三种方式
  • 原文地址:https://www.cnblogs.com/sun-ye/p/14992167.html
Copyright © 2011-2022 走看看