[C in ASM(ARM64)]第一章 一些实例

[C in ASM(ARM64)]第一章 一些实例

C程序设计语言的汇编解释 第一章 一些实例

不多废话,直接从实例开始吧!

1.1 开始

讲语言的第一个例子自然是在控制台打印:

hello, world

想必大家都可以很轻易的用C写出如下代码:

#include <stdio.h>

main()
{
    printf("hello, world\n");
}

将上述代码保存至helloworld.c, 并使用clang进行编译:

clang helloworld.c

得到a.out产物, 在命令行执行它, 将打印出如下内容:

hello, world

咳咳, 这并不是想要的结果, 还是祭出clang汇编吧:

// 注意,以下代码将默认生成pc版的汇编指令
clang -S helloworld.c
// ARM64汇编需要如下命令,指定架构和系统头文件所在的目录,请务必将isysroot的sdk版本修改为自己xcode中存在的版本!
clang -S -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.1.sdk helloworld.c

该指令会生成同名文件helloworld.s, 其内容如下:

        .section        __TEXT,__text,regular,pure_instructions
        .ios_version_min 11, 1
        .globl  _main
        .p2align        2
_main:                                  ; @main
; BB#0:
        sub     sp, sp, #32             ; =32
        stp     x29, x30, [sp, #16]     ; 8-byte Folded Spill
        add     x29, sp, #16            ; =16
        adrp    x0, l_.str@PAGE
        add     x0, x0, l_.str@PAGEOFF
        bl      _printf
        mov     w8, #0
        stur    w0, [x29, #-4]          ; 4-byte Folded Spill
        mov      x0, x8
        ldp     x29, x30, [sp, #16]     ; 8-byte Folded Reload
        add     sp, sp, #32             ; =32
        ret

        .section        __TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
        .asciz  "hello, world\n"


.subsections_via_symbols

代码中类似.section.globl等以'.'开头的, 被称之为编译器指令, 用于告知编译器相关的信息或者进行特定操作。

类似_main:或l_.str:的,被称之为标签(label), 用于辅助定位代码或者资源地址, 也便于开发者理解和记忆。

类似pushqmovq的, 被称之为汇编指令, 它们会被汇编器编译为机器代码, 最终被cpu所执行。

上述代码中, 用.section指令生成了两个节:

* __TEXT,__text用来存放代码指令, 代码一般都放在这一节。
* __TEXT,__cstring用来存放c string也就是C语言字符串, 可以看到'hello, world\n'字符串就存放在这里。'.asciz'表示零结尾的字符串。

'hello, world\n'字符串前一行是一个标签(label)'l_.str',在之前的代码中'adrp x0, l_.str@PAGE'指令引用了'L_.str'这个标签,标签之后的'@PAGE'表示获取标签所在的页的地址(一个按0x1000或0x4000对齐的地址),下一条指令'add x0, x0, l_.str@PAGEOFF'中的'@PAGEOFF'表示获取标签地址对应页地址的偏移,在经过汇编器汇编后会将标汇编为字符串所存放的地址,让程序可以定位到字符串。

指令'bl _printf'调用`_printf`方法,它会将'x0'寄存器里的内容作为第一个参数(里面存放的是"hello, world\n"字符串的地址)。按照ARM64的Calling Convention,整形的参数前8个会按顺序放到'x0-x7'寄存器里,超过八个的放到栈上传递(详见文末参考1)。

代码中的如下部分被称之为方法头(prologue), 用于保存上一个方法调用栈帧的帧头及预留部分栈空间用于局部变量:

        sub     sp, sp, #32             ; =32
        stp     x29, x30, [sp, #16]     ; 8-byte Folded Spill
        add     x29, sp, #16            ; =16


代码中如下部分被称之为方法尾(epilogue), 用于取出方法头中栈帧信息及方法的返回地址, 并将栈恢复到方法调用前的位置:

        ldp     x29, x30, [sp, #16]     ; 8-byte Folded Reload
        add     sp, sp, #32             ; =32
        ret

调用栈的栈帧(stack frame)可以用来回溯调用栈,可以用于生成调用栈。

1.2 变量和算术表达式

下一段程序用于输出华氏温度和摄氏温度的值的对应关系, 其运算公式为'C=(5/9)(F-32)', 程序输出如下结果:

0	-17
20	-6
40	4
60	15
80	26
100	37
120	48
140	60
160	71
180	82
200	93
220	104
240	115
260	126
280	137
300	148

对应的代码如下:

#include <stdio.h>

/* print Fahrenheit-Celsius table for fahr = 0, 20, ..., 300 */ 
main() {
    int fahr, celsius;
    int lower, upper, step;

    lower = 0;    /* lower limit of temperature scale */
    upper = 300;  /* upper limit */
    step = 20;     /* step size */

    fahr = lower;
    while (fahr <= upper) {
        celsius = 5 * (fahr-32) / 9;
        printf("%d\t%d\n", fahr, celsius);
        fahr = fahr + step;
    }
}

这段代码的汇编结果如下:

	.section	__TEXT,__text,regular,pure_instructions
	.ios_version_min 11, 1
	.globl	_main
	.p2align	2
_main:                                  ; @main
; BB#0:
	sub	sp, sp, #64             ; =64
	stp	x29, x30, [sp, #48]     ; 8-byte Folded Spill
	add	x29, sp, #48            ; =48
	mov	w8, #20
	mov	w9, #300
	stur	wzr, [x29, #-4]
	stur	wzr, [x29, #-16]
	stur	w9, [x29, #-20]
	str	w8, [sp, #24]
	ldur	w8, [x29, #-16]
	stur	w8, [x29, #-8]
LBB0_1:                                 ; =>This Inner Loop Header: Depth=1
	ldur	w8, [x29, #-8]
	ldur	w9, [x29, #-20]
	cmp		w8, w9
	b.gt	LBB0_3
; BB#2:                                 ;   in Loop: Header=BB0_1 Depth=1
	mov	w8, #5
	ldur	w9, [x29, #-8]
	sub	w9, w9, #32             ; =32
	mul		w8, w8, w9
	mov	w9, #9
	sdiv	w8, w8, w9
	stur	w8, [x29, #-12]
	ldur	w8, [x29, #-8]
	ldur	w9, [x29, #-12]
                                        ; implicit-def: %X0
	mov	 x0, x9
	mov	 x10, sp
	str	x0, [x10, #8]
                                        ; implicit-def: %X0
	mov	 x0, x8
	str		x0, [x10]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	ldur	w8, [x29, #-8]
	ldr	w9, [sp, #24]
	add		w8, w8, w9
	stur	w8, [x29, #-8]
	str	w0, [sp, #20]           ; 4-byte Folded Spill
	b	LBB0_1
LBB0_3:
	ldur	w0, [x29, #-4]
	ldp	x29, x30, [sp, #48]     ; 8-byte Folded Reload
	add	sp, sp, #64             ; =64
	ret

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


.subsections_via_symbols


这里先介绍一个重点:局部变量是放在栈上的!
其中的关键点和解释如下:

        sub	sp, sp, #64             ; =64
	stp	x29, x30, [sp, #48]     ; 8-byte Folded Spill
	add	x29, sp, #48            ; =48

prologue部分,把sp下移64 byte,并往sp最顶部的16位存入上一个stack frame的fp和lr,剩下48个byte,然后把x29(也就是fp)设为栈顶(这个是ARM64的约定)。然后就是局部变量处理:

        mov	w8, #20
	mov	w9, #300
	stur	wzr, [x29, #-4]
	stur	wzr, [x29, #-16]
	stur	w9, [x29, #-20]
	str	w8, [sp, #24]
	ldur	w8, [x29, #-16]
	stur	w8, [x29, #-8]

由于ARM64没有指令可以直接把常数放入内存,所以编译器采用了把常数先放入寄存器,再从寄存器中存入内存的方式来实现。由于我们在对c代码做汇编时没有指定优化级别,默认生成的汇编代码未做优化,其代码顺序和c源代码会高度相关。

    int fahr, celsius;
    int lower, upper, step;

这里申明的局部变量会按照顺序在栈上预留空间,也就是之前的48byte中的某些部分。这里还隐含了一个局部变量'int return_value;'的声明在最前面,因为main方法实际的返回值是int,而不是void,编译器自动会加上返回值的处理,返回值默认是0。

那么栈剩余的空间和局部变量的对应关系会变成这样:

x29-4 = sp+44 = return_value = 0
x29-8 = sp+40 = fahr
x29-12 = sp+36 = celsius
x29-16 = sp+32 = lower
x29-20 = sp+28 = upper
x29-24 = sp+24 = step

首先是c源代码用到的两个常量20和300分别放入w8和w9寄存器。然后把wzr(零寄存器,里面存的是0)的内容分别存入'x29-4'(也就是return_value)和'x29-16'(也就是lower)位置。再然后把w9的内容也就是常量300存入'x29-20'(也就是upper),再然后把w8的内容也就是20存入'sp+24'也就是step中。再然后用ldur指令从'x29-16'也就是lower取出值放入'x29-8'(fahr变量)中完成'fahr = lower'。

后面开始进入循环:

LBB0_1:                                 ; =>This Inner Loop Header: Depth=1
	ldur	w8, [x29, #-8]
	ldur	w9, [x29, #-20]
	cmp		w8, w9
	b.gt	LBB0_3

首先是标签LBB0_1处,后面的注释已经提到这里是循环头。首先从'x29-8'(fahr)取出值放入w8,再从'x29-20'(upper)处取出值放入w9,再把x8和x9做比较,如果结果是gt(大于)则跳转到标签LBB0_3也就是循环结尾。从C源代码中可以看到这里是做了'fahr<=upper'的判断,而汇编代码的现实是如果大于就跳到循环结尾,否则继续往下走进入循环体。

接下来是循环体里的'celsius = 5 * (fahr-32) / 9;'这句:

; BB#2:                                 ;   in Loop: Header=BB0_1 Depth=1
	mov	w8, #5
	ldur	w9, [x29, #-8]
	sub	w9, w9, #32             ; =32
	mul		w8, w8, w9
	mov	w9, #9
	sdiv	w8, w8, w9
	stur	w8, [x29, #-12]

首先把数字5放入寄存器w8,然后从'x29-8'(fahr)取出值放入w9,然后'w9=w9-32',再'w8=w8*w9'完成了'5 * (fahr-32)'。然后把数字存入w9,然后'w8=w8/w9',完成'5 * (fahr-32) / 9',结果w8放入'x29-12'(celsius)中。

接下来是'printf'这句:

	ldur	w8, [x29, #-8]
	ldur	w9, [x29, #-12]
                                        ; implicit-def: %X0
	mov	 x0, x9
	mov	 x10, sp
	str	x0, [x10, #8]
                                        ; implicit-def: %X0
	mov	 x0, x8
	str		x0, [x10]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf

首先取出fahr(放入w8)和celsius(放入w9),把x9(w9和x9是同一个寄存器,x是整个64位,w是低32位)移入x0。然后把sp移入x10,把x0(也就是celsius)的值存入'x10+8'(也就是sp+8)指向的内存中,作为printf的第三个参数。然后同样的操作把fahr放入'sp'指向的内存中。接下来'adrp'和'add'两句取出printf的第一个参数(也就是format)放入x0中,然后调用'_printf方法'。注意,这里的'_printf'也是一个标签,它指向了'printf'的代码所在的地址,在编译后会被直接替换为一个地址。再注意,前面提到方法调用的参数传递一般是依次放在x0-x7然后才是栈上,而'printf'从第二参数开始就放在了栈上,是因为printf是可变参数方法,可变参数部分都是存在栈上的(详见文末参考2)。

再接下来就是循环体内最后一句:

	ldur	w8, [x29, #-8]
	ldr	w9, [sp, #24]
	add		w8, w8, w9
	stur	w8, [x29, #-8]
	str	w0, [sp, #20]           ; 4-byte Folded Spill
	b	LBB0_1

取出fahr和celsius,相加,放回fahr,然后调回循环头'LBB0_1'处。

然后就是epilog,这里就不多做说明:

LBB0_3:
	ldur	w0, [x29, #-4]
	ldp	x29, x30, [sp, #48]     ; 8-byte Folded Reload
	add	sp, sp, #64             ; =64
	ret


1.3 for循环

在1.2节的例子中,用的是while循环,实际上也可以用for循环来实现:

#include <stdio.h>

/* print Fahrenheit-Celsius table */ 
main() {
    int fahr;
		
    for (fahr = 0; fahr <= 300; fahr = fahr + 20) {
        printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
    }
}

其汇编实现如下:

	.section	__TEXT,__text,regular,pure_instructions
	.ios_version_min 11, 1
	.section	__TEXT,__literal8,8byte_literals
	.p2align	3
lCPI0_0:
	.quad	4603179219131243634     ; double 0.55555555555555558
	.section	__TEXT,__text,regular,pure_instructions
	.globl	_main
	.p2align	2
_main:                                  ; @main
; BB#0:
	sub	sp, sp, #48             ; =48
	stp	x29, x30, [sp, #32]     ; 8-byte Folded Spill
	add	x29, sp, #32            ; =32
	stur	wzr, [x29, #-4]
	stur	wzr, [x29, #-8]
LBB0_1:                                 ; =>This Inner Loop Header: Depth=1
	ldur	w8, [x29, #-8]
	cmp		w8, #300        ; =300
	b.gt	LBB0_4
; BB#2:                                 ;   in Loop: Header=BB0_1 Depth=1
	adrp	x8, lCPI0_0@PAGE
	ldr	d0, [x8, lCPI0_0@PAGEOFF]
	ldur	w9, [x29, #-8]
	ldur	w10, [x29, #-8]
	sub	w10, w10, #32           ; =32
	scvtf	d1, w10
	fmul	d0, d0, d1
	mov	 x8, sp
	str	d0, [x8, #8]
                                        ; implicit-def: %X0
	mov	 x0, x9
	str		x0, [x8]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	stur	w0, [x29, #-12]         ; 4-byte Folded Spill
; BB#3:                                 ;   in Loop: Header=BB0_1 Depth=1
	ldur	w8, [x29, #-8]
	add	w8, w8, #20             ; =20
	stur	w8, [x29, #-8]
	b	LBB0_1
LBB0_4:
	ldur	w0, [x29, #-4]
	ldp	x29, x30, [sp, #32]     ; 8-byte Folded Reload
	add	sp, sp, #48             ; =48
	ret

	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"%3d %6.1f\n"

.subsections_via_symbols

源代码中有一个常数运算'5.0/9.0',被编译器给优化成了这样:

	.section	__TEXT,__literal8,8byte_literals
	.p2align	3
lCPI0_0:
	.quad	4603179219131243634     ; double 0.55555555555555558

标签'lCPI0_0'处,用'.quad'指令带上了一串数字'4603179219131243634',从后面注释可以看出,它是double数字'0.55555555555555558'的内存表现形式的十进制形式。

前面的'.section'指明这个数字放在'__TEXT,__literal8'节(8byte长度的常量),'.p2align'指明按2的3次方也就是8byte对齐。

for循环的头部判断是否需要结束循环,同样使用的'b.gt'指令:

LBB0_1:                                 ; =>This Inner Loop Header: Depth=1
	ldur	w8, [x29, #-8]
	cmp		w8, #300        ; =300
	b.gt	LBB0_4

而'fahr=fahr+20'的操作是在循环的尾部做的:

; BB#3:                                 ;   in Loop: Header=BB0_1 Depth=1
	ldur	w8, [x29, #-8]
	add	w8, w8, #20             ; =20
	stur	w8, [x29, #-8]
	b	LBB0_1

在代码中用到了d系列寄存器,它的长度是一个double的长度也就是16byte,和x系列寄存器和w系列寄存器共用类似,d系列寄存器是'b,h,s,d,q'也就是'byte, half, single, double, quad'共用的,q寄存器有128位,d用它的低64位,s用它的低32位,h用它的低16位,b用它的低8位。另外q和v(vector)系列寄存器是共用的,这里暂时不对v系列寄存器展开讨论。

1.4 符号常量

同样从一个示例开始吧:

#include <stdio.h>

#define LOWER 0    /* lower limit of table */
#define UPPER 300  /* upper limit */
#define STEP  20   /* step size */

/* print Fahrenheit-Celsius table */
main() {
    int fahr;
		
    for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
        printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}

上面的代码里有用到宏'LOWER/UPPER/STEP',在编译过程中会被预处理器替换到源代码中。以上代码保存为'f2cforpreprocessor.c',可以通过如下命令进行预处理(preprocessor):

.....省略头文件的预处理部分.....
main() {
    int fahr;
		
    for (fahr = 0; fahr <= 300; fahr = fahr + 20)
        printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}

'#include'宏把'stdio.h'展开到源代码中,使得屏幕上会输出一大堆代码,这里我们不多做介绍,有兴趣的读者可以自己阅读。而代码中的'LOWER/UPPER/STEP'被分别替换为了常量'0/300/20'。

1.5 数组

同样从一段示例代码开始:

#include <stdio.h>

main()
{
    int ndigit[10];
}

其汇编代码如下:

	.section	__TEXT,__text,regular,pure_instructions
	.ios_version_min 11, 1
	.globl	_main
	.p2align	2
_main:                                  ; @main
; BB#0:
	sub	sp, sp, #80             ; =80
	stp	x29, x30, [sp, #64]     ; 8-byte Folded Spill
	add	x29, sp, #64            ; =64
	adrp	x8, ___stack_chk_guard@GOTPAGE
	ldr	x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
	ldr		x8, [x8]
	adrp	x9, ___stack_chk_guard@GOTPAGE
	ldr	x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
	ldr		x9, [x9]
	stur	x9, [x29, #-8]
	adrp	x9, ___stack_chk_guard@GOTPAGE
	ldr	x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
	ldr		x9, [x9]
	ldur	x10, [x29, #-8]
	cmp		x9, x10
	str	x8, [sp, #8]            ; 8-byte Folded Spill
	b.ne	LBB0_2
; BB#1:
	mov	w8, #0
	mov	 x0, x8
	ldp	x29, x30, [sp, #64]     ; 8-byte Folded Reload
	add	sp, sp, #80             ; =80
	ret
LBB0_2:
	bl	___stack_chk_fail


.subsections_via_symbols

首先是空间开辟:

; BB#0:
	sub	sp, sp, #80             ; =80
	stp	x29, x30, [sp, #64]     ; 8-byte Folded Spill
	add	x29, sp, #64            ; =64

栈上开辟了80个byte的空间,64byte给局部变量,用1.2节中介绍到的局部变量空间对应关系:

int return_value;
int ndigit[10];

总共11个int占44byte空间,但却给了64byte空间,为什么呢?继续往下看:

	adrp	x8, ___stack_chk_guard@GOTPAGE
	ldr	x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
	ldr		x8, [x8]
	adrp	x9, ___stack_chk_guard@GOTPAGE
	ldr	x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
	ldr		x9, [x9]
	stur	x9, [x29, #-8]

这里取了一个'___stack_chk_guard'的标签里的内容分别放入了x8和x9,并且x9存入了'x29-8'里(x29是栈顶),那栈目前的内容就变成了这样:

x29-4 = sp+60 = return_value = 0
x29-8 = sp+56 = ___stack_chk_guard

接下来继续看:

	adrp	x9, ___stack_chk_guard@GOTPAGE
	ldr	x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
	ldr		x9, [x9]
	ldur	x10, [x29, #-8]
	cmp		x9, x10
	str	x8, [sp, #8]            ; 8-byte Folded Spill
	b.ne	LBB0_2

又取了一次'___stack_chk_guard'到x9,再从'x29-8'里面取出之前存进去的'___stack_chk_guard'值做对比,如果不相等,则跳转到'LBB0_2':

LBB0_2:
	bl	___stack_chk_fail

就调到'___stack_chk_fail'方法,然后挂掉了。那么问题来了,为什么'___stack_chk_guard'和'___stack_chk_guard'的值会不相等呢?注意力回到'x29-8',如果它的内容因为代码bug或者恶意代码导致内存数据被破坏(数组越界写访问),就会导致失败。这个检查,一定程度保护了栈数据的安全。同时倒数第二句里面又在'sp+8'位置也插了个'___stack_chk_guard'进去,内存数据就变成了这样:

x29-4 = sp+60 = return_value = 0
x29-8 = sp+56 = ___stack_chk_guard
...ndigits...
x29-56 = sp+8 = ___stack_chk_guard

'x29-56 - (x29-8) - 8 = 40'(这里的8是___stack_chk_guard的数据大小),刚好10个整数的长度,40byte。

1.6 方法和参数传递

前面的小节中讨论的内容都是语句级别的,那么方法的表现形式是什么呢?上代码:

#include <stdio.h>

int power(int m, int n); /* test power function */

int main() {
    return power(2,1);
}

int power(int base, int n) {
    return base;
}

该代码的汇编实现如下:

	.section	__TEXT,__text,regular,pure_instructions
	.ios_version_min 11, 1
	.globl	_main
	.p2align	2
_main:                                  ; @main
; BB#0:
	sub	sp, sp, #32             ; =32
	stp	x29, x30, [sp, #16]     ; 8-byte Folded Spill
	add	x29, sp, #16            ; =16
	orr	w0, wzr, #0x2
	orr	w1, wzr, #0x1
	stur	wzr, [x29, #-4]
	bl	_power
	ldp	x29, x30, [sp, #16]     ; 8-byte Folded Reload
	add	sp, sp, #32             ; =32
	ret

	.globl	_power
	.p2align	2
_power:                                 ; @power
; BB#0:
	sub	sp, sp, #16             ; =16
	str	w0, [sp, #12]
	str	w1, [sp, #8]
	ldr	w0, [sp, #12]
	add	sp, sp, #16             ; =16
	ret


.subsections_via_symbols

这里除了'_main'标签外,代码节里面还多了'_power'标签,在要调用'_power'方法的地方可以通过branch系列指令'b/bl/br/blr'(对ARM64指令有兴趣的读者可以参见文末参考3)。来调到对应的方法,比如'_main'里面的'bl _power'语句。

power方法的两个参数2和1分别放入了w0和w1中(x0和x1),和前文提到的calling convention一致(文末参考1、2)。

1.7 外部变量和作用域

分别有两个文件extern1.c内容如下:

#include <stdio.h>

void copy(void);
char line[1000];
int main() {
    line[0] = 'a';
    copy();
}

和extern2.c内容如下:

void copy()
{
    extern char line[];
    line[1] = line[0];
}

他们均能被单独编译为汇编文件'extern1.s':

	.section	__TEXT,__text,regular,pure_instructions
	.ios_version_min 11, 1
	.globl	_main
	.p2align	2
_main:                                  ; @main
; BB#0:
	stp	x29, x30, [sp, #-16]!   ; 8-byte Folded Spill
	mov	 x29, sp
	mov	w8, #97
	adrp	x9, _line@GOTPAGE
	ldr	x9, [x9, _line@GOTPAGEOFF]
	strb		w8, [x9]
	bl	_copy
	mov	w8, #0
	mov	 x0, x8
	ldp	x29, x30, [sp], #16     ; 8-byte Folded Reload
	ret

	.comm	_line,1000,0            ; @line

.subsections_via_symbols

及'extern2.s':

	.section	__TEXT,__text,regular,pure_instructions
	.ios_version_min 11, 1
	.globl	_copy
	.p2align	2
_copy:                                  ; @copy
; BB#0:
	adrp	x8, _line@GOTPAGE
	ldr	x8, [x8, _line@GOTPAGEOFF]
	ldrb		w9, [x8]
	strb	w9, [x8, #1]
	ret

.subsections_via_symbols

在'extern1.s'中'.comm _line,1000,4'会为'_line'标签在全局'__DATA,__common'节中分配一段空间,使得在'extern2.s'中被'adrp x8, _line@GOTPAGE'找到并访问。

而在'extern2.s'中'.globl _copy'会将'_copy'标签暴露在全局,使得在'extern1.s'中被'bl _copy'访问到。

各符号所对应的具体地址会在链接(link)的过程中被确定和关联。执行指令'clang -arch arm64 -O0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.1.sdk extern1.c extern2.c'编译出产物'a.out', 通过otool工具可以查看编译后的产物的信息,如'otool -tv a.out'可以查看'__TEXT'段的反汇编:

$clang -arch arm64 -O0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.1.sdk extern1.c extern2.c
$otool -tv a.out
a.out:
(__TEXT,__text) section
_main:
0000000100007f78	stp	x29, x30, [sp, #-0x10]!
0000000100007f7c	mov	 x29, sp
0000000100007f80	mov	w8, #0x61
0000000100007f84	adrp	x9, 1 ; 0x100007000
0000000100007f88	add	x9, x9, #0x0
0000000100007f8c	strb		w8, [x9]
0000000100007f90	bl	0x100007fa4
0000000100007f94	mov	w8, #0x0
0000000100007f98	mov	 x0, x8
0000000100007f9c	ldp	x29, x30, [sp], #0x10
0000000100007fa0	ret
_copy:
0000000100007fa4	adrp	x8, 1 ; 0x100007000
0000000100007fa8	add	x8, x8, #0x0
0000000100007fac	ldrb		w9, [x8]
0000000100007fb0	strb	w9, [x8, #0x1]
0000000100007fb4	ret

可以看到copy方法中的line引用以及main方法中对copy的调用,都已经被替换为了对应的地址。

同时可以通过'nm a.out'命令,查看可执行文件中的所有符号(标签,在编译后我们称之为符号):

$nm a.out
0000000100000000 T __mh_execute_header
0000000100007fa4 T _copy
0000000100008000 S _line
0000000100007f78 T _main
                 U dyld_stub_binder

前面用到的'_copy/_line/_main'全部在列,里面'T'代表代码节的符号,'U'代表未定义,'S'代表其他(更多解释参见执行命令'man nm'的结果)。

参考文献:

  1. ARM64 Function Calling Conventions
  2. Procedure Call Standard for the ARM 64-bit Architecture (AArch64)
  3. ARMv8 Instruction Set Overview
编辑于 2017-11-19

文章被以下专栏收录