C程序设计语言的汇编解释(ARM64版) 第一章 一些实例
不多废话,直接从实例开始吧!
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等以'.'开头的, 被称之为汇编器指令, 用于告知汇编器相关的信息(如'.section'后面的节的名称和属性)或者进行特定操作。
类似_main:或l_.str:的,被称之为标签(label), 用于辅助定位代码或者资源地址, 也便于开发者理解和记忆。
类似pushq或movq的, 被称之为汇编指令, 它们会被汇编器编译为机器代码, 最终被cpu所执行。
上述代码中, 用.section指令生成了两个节:
-
__TEXT,__text
用来存放代码指令, 代码一般都放在这一节。 -
__TEXT,__cstring
用来存放c string也就是C语言字符串, 可以看到'hello, worldn'字符串就存放在这里。'.asciz'表示零结尾的字符串。
'hello, worldn'字符串前一行是一个标签(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, worldn"字符串的地址)。按照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=w8w9'完成了'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'的结果)。
参考文献:
- ARM64 Function Calling Conventions: https://developer.apple.com/library/content/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html
- Procedure Call Standard for the ARM 64-bit Architecture (AArch64): http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
- ARMv8 Instruction Set Overview: http://profsite.um.ac.ir/~shoraka/ARMInstructionSet.pdf