September 17, 2020

macOS assembly language program analysis (m1 chip)

一个 macOS 汇编的分析(M1 芯片)

1. 代码

源文件 main.c 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void swap(long long int v[], size_t k) {
__asm__ __volatile__ (
"lsl x10, x1, #3\n"
"add x10, x0, x10\n"
"ldur x9, [x10, #0]\n"
"ldur x11, [x10, #8]\n"
"stur x11, [x10, #0]\n"
"stur x9, [x10, #8]");
}

int main(int argc, const char * argv[]) {
// insert code here...
long long int num[3] = {1, 2, 3};
swap(num, 1);
printf("%lld %lld\n", num[1], num[2]);
return 0;
}

swap 函数实现了语句 v[k] <--> v[k+1],将两个元素进行交换,注意,这里使用了内联汇编。

执行以后的 main.s 文件代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
	.section	__TEXT,__text,regular,pure_instructions
.build_version macos, 11, 0 sdk_version 11, 3
.globl _swap ; -- Begin function swap
.p2align 2
_swap: ; @swap
.cfi_startproc
; %bb.0:
sub sp, sp, #16 ; =16
.cfi_def_cfa_offset 16
str x0, [sp, #8]
str x1, [sp]
; InlineAsm Start
lsl x10, x1, #3
add x10, x0, x10
ldur x9, [x10]
ldur x11, [x10, #8]
stur x11, [x10]
stur x9, [x10, #8]
; InlineAsm End
add sp, sp, #16 ; =16
ret
.cfi_endproc
; -- End function
.globl _main ; -- Begin function main
.p2align 2
_main: ; @main
.cfi_startproc
; %bb.0:
sub sp, sp, #80 ; =80
stp x29, x30, [sp, #64] ; 16-byte Folded Spill
add x29, sp, #64 ; =64
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
adrp x8, ___stack_chk_guard@GOTPAGE
ldr x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
ldr x8, [x8]
stur x8, [x29, #-8]
str wzr, [sp, #28]
str w0, [sp, #24]
str x1, [sp, #16]
adrp x8, l___const.main.num@PAGE
add x8, x8, l___const.main.num@PAGEOFF
ldr q0, [x8]
add x0, sp, #32 ; =32
str q0, [sp, #32]
ldr x8, [x8, #16]
str x8, [sp, #48]
mov x1, #1
bl _swap
ldr x8, [sp, #40]
ldr x9, [sp, #48]
adrp x0, l_.str@PAGE
add x0, x0, l_.str@PAGEOFF
mov x10, sp
str x8, [x10]
str x9, [x10, #8]
bl _printf
adrp x8, ___stack_chk_guard@GOTPAGE
ldr x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
ldr x8, [x8]
ldur x9, [x29, #-8]
subs x8, x8, x9
b.ne LBB1_2
; %bb.1:
mov w8, #0
mov x0, x8
ldp x29, x30, [sp, #64] ; 16-byte Folded Reload
add sp, sp, #80 ; =80
ret
LBB1_2:
bl ___stack_chk_fail
.cfi_endproc
; -- End function
.section __TEXT,__const
.p2align 3 ; @__const.main.num
l___const.main.num:
.quad 1 ; 0x1
.quad 2 ; 0x2
.quad 3 ; 0x3

.section __TEXT,__cstring,cstring_literals
l_.str: ; @.str
.asciz "%lld %lld\007n"

.subsections_via_symbols

2. 分析

1
2
.section	__TEXT,__text,regular,pure_instructions
.build_version macos, 11, 0 sdk_version 11, 3

首先,定义了一个节(section)属于__Text 段的 _text 节,属于常规(regular)类型,属性为纯指令(pure_instructions)。

随后指定了构建系统版本(macOS 11.0)和 SDK 版本(11.3)。

然后开始定义 swap 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	.globl	_swap                           ; -- Begin function swap
.p2align 2
_swap: ; @swap
.cfi_startproc
; %bb.0:
sub sp, sp, #16 ; =16
.cfi_def_cfa_offset 16
str x0, [sp, #8]
str x1, [sp]
; InlineAsm Start
lsl x10, x1, #3
add x10, x0, x10
ldur x9, [x10]
ldur x11, [x10, #8]
stur x11, [x10]
stur x9, [x10, #8]
; InlineAsm End
add sp, sp, #16 ; =16
ret
.cfi_endproc

首先前三行申明了函数名称和指令对齐方式,其中.p2align 伪指令指定指令以 2^x 字节对齐,由于 arm 指令集定长为 32 位,即 4 字节,所以对齐方式为 4 字节对齐。.cfi_startproc.cfi_endproc定义了一段代码的开始与结束,在这之间写汇编指令。

这里要说明的是,arm64 要求栈的对齐方式为16字节,由于 swap 的这两个参数均为 64 位,即 8 字节,所以只需要开一个 16 字节的栈就可以了。和 x86 类似,栈也是从高地址向低地址生长,且参数由右向左入栈,所以 x0 存放的是 v 数组的基地址,x1 存放的是 k 的值,由于 long long int 数据类型一个占 8 字节,所以需要将 k * 8 才能找到 &v[k]。然后通过一系列 load/store 交换内存这两个元素的值,最后清空栈,也就是将 sp 加 16,然后通过 ret 指令设置 pc 程序计数寄存器返回到调用者。

然后开始定义 main 函数。在操作系统看来,main 函数和其他函数并没有什么不同,由于它是用户编写程序的入口,调用者是操作系统,执行完毕或出现异常之后要返回到操作系统,所以 main 函数第一步需要做的是定义对栈,然后将操作系统在执行 main 函数的 fp 信息和 sp 信息存储下来。

1
2
3
4
5
6
sub	sp, sp, #80                     ; =80
stp x29, x30, [sp, #64] ; 16-byte Folded Spill
add x29, sp, #64 ; =64
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16

首先是建立堆栈,这里的对栈也是 16 字节对齐。这里建立了一个五个元素的对栈。其中,第一个元素有 16 字节,可以存放 fp 和 lr 的值。然后栈顶指向第四个元素的下面第一个字节表示现在堆栈为空。.cfi_def_cfa 其中 cfi 指的是 call from info,是 DWARF 2.0 规定的堆栈信息。cfa 指的是 canonical frame address 规范堆栈框,这是定义了该程序所有函数的基地址,其基地址是原先 x29 寄存器加 16,即我们开的80字节堆栈的栈底。然后又定义了 fp 和 sp 相对于 cfa 的偏移。整个对栈此时的情况如下。

相对地址(以执行程序前 sp 地址为相对零地址) 存放内容 字节
0
+16
+32
+48
+64 FP 低 8 字节
LR 高 8 字节
+80 CFA

然后通过暂存寄存器 x8 去检查堆栈是否发生错误,指令如下。

1
2
3
adrp	x8, ___stack_chk_guard@GOTPAGE
ldr x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
ldr x8, [x8]

首先,先从编译器堆栈保护函数处获取页号将基地址对应到 x8 上,然后加载页偏移处 x8 的值,这也是一个地址,最后通过这个地址找到该值并且将其送入到 x8 中。接着,这几步看不大懂。

1
2
3
4
stur	x8, [x29, #-8]
str wzr, [sp, #28]
str w0, [sp, #24]
str x1, [sp, #16]

然后在堆栈中存储程序中的数据

1
2
3
4
5
6
7
8
adrp	x8, l___const.main.num@PAGE
add x8, x8, l___const.main.num@PAGEOFF
ldr q0, [x8]
add x0, sp, #32 ; =32
str q0, [sp, #32]
ldr x8, [x8, #16]
str x8, [sp, #48]
mov x1, #1

前两句赋给 x8 寄存器 &num,然后将从 sp + 32 开始的 4 个 long long int 大小的地方存 num 数组。注意,这里的存储是小端存储。

之后就是执行 printf 函数了。

3. 从 .s 编译到可执行程序

.s –> .o

1
as -o <filename>.o <filename>.s

.o –> executive file (binary file)

1
2
3
4
5
ld -o <filename> <filename>.o
-lSysten
-syslibroot `xcrun -sdk macosx --show-sdk-path`
-e start
-arch arm64

About this Post

This post is written by Chen Li, licensed under CC BY-NC 4.0.