我們都認(rèn)為C語言是一種非常靜態(tài)
的語言坑匠,幾乎沒有什么動態(tài)特性后室,同時往往在編譯器就決定了整個運(yùn)行方式,運(yùn)行期是很難改變其運(yùn)行狀態(tài)的惊来。其實(shí)C語言也是可以比較動態(tài)的,只是由于C語言是一個跨平臺兼容語言棺滞,每個平臺都有不同的實(shí)現(xiàn)裁蚁,其動態(tài)化很難統(tǒng)一。這里我們看看在AArch64平臺上的動態(tài)化實(shí)現(xiàn)继准。
其他語言的動態(tài)化
腳本語言是非常具有動態(tài)特性的枉证,其中典型的js就可以如下方式動態(tài)調(diào)用方法。
function hello() {
console.log('hello world')
}
eval('hello()')
平時開發(fā)常用的Objc也有一定的動態(tài)特性移必,比如NSInvocation
和
- (id)performSelector:(SEL)aSelector;
都可以通過方法名稱來調(diào)用刽严。
那么我們來看看C語言的表現(xiàn)。
C語言的動態(tài)化
根據(jù)上兩篇內(nèi)容避凝,我們了解了iOS/Mac系統(tǒng)的執(zhí)行文件格式MachO舞萄,而linux常用的ELF也是類似,執(zhí)行代碼都在TEXT
段管削,如果我們要執(zhí)行對應(yīng)的方法倒脓,我們只需要拿到對應(yīng)的地址(也就是函數(shù)指針)就行了。
那么如何從字符串找到對應(yīng)的地址呢含思?這就涉及到函數(shù)符號表了崎弃,根據(jù)上篇的內(nèi)容,不難找到其對應(yīng)的函數(shù)指針含潘,這里系統(tǒng)也給我們提供了一個封裝好的方法饲做。
NAME
dlsym -- get address of a symbol
SYNOPSIS
#include <dlfcn.h>
void*
dlsym(void* handle, const char* symbol);
DESCRIPTION
dlsym() returns the address of the code or data location specified by the null-terminated character string symbol. Which libraries and bundles are searched depends on the handle parameter.
有了函數(shù)指針之后,只需要將我們的參數(shù)填入對應(yīng)位置遏弱,我們就可以實(shí)現(xiàn)方法調(diào)用了盆均。如何填入?yún)?shù)呢?根據(jù)之前的討論和aapcs64ARM官方文檔的說明漱逸,我們可以按照這種思路去填入?yún)?shù)泪姨。
這里我們簡單的把所有參數(shù)都認(rèn)為是int64(或者說void *)類型游沿,這樣我們可以把以上邏輯簡化為:
- 按x0-x7順序填入寄存器
- 剩下的都放入棧中
這里設(shè)計個簡單的動態(tài)調(diào)用接口:
extern void dynamic_call_func_name(const char *func, int64_t argc, int64_t *args) {
void *funcPtr = dlsym(RTLD_DEFAULT, func);
dynamic_call_func((uintptr_t)funcPtr, argc, args);
}
extern void dynamic_call_func(uintptr_t func, int64_t argc, int64_t *args);
以及測試函數(shù):
void one_arg(int64_t a1);
void two_arg(int64_t a1, int64_t a2);
void eight_arg(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8);
void nine_arg(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8, int64_t a9);
void ten_arg(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10);
void eleven_arg(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10, int64_t a11);
void more_arg(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, int64_t a8,
int64_t aa1, int64_t aa2, int64_t aa3, int64_t aa4, int64_t aa5, int64_t aa6, int64_t aa7, int64_t aa8);
那么動態(tài)調(diào)用可以寫作:
int64_t args[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
};
dynamic_call_func_name("eleven_arg", sizeof(args)/sizeof(int64_t), args);
這樣我們就實(shí)現(xiàn)了C語言的動態(tài)調(diào)用了。比如可以從其他端獲得方法名和參數(shù)列表肮砾,就可以直接調(diào)用C方法了诀黍。這也是一些高級語言調(diào)用C語言的實(shí)現(xiàn)方式(cpython),以及一些rpc的實(shí)現(xiàn)方案仗处。
接下來我們來看看如何填入?yún)?shù)眯勾,dynamic_call_func
的實(shí)現(xiàn)方式。
動態(tài)設(shè)置參數(shù)
這里我們只能通過匯編來設(shè)置參數(shù)了婆誓。
.align 4
// C方法會自動加上前綴`_`
.global _dynamic_call_func
_dynamic_call_func:
// if func == NULL then return
cbz x0, DCReturnZero
// 申請棾曰罚空間,0x10用于緩存fp和lr旷档,剩下的用于臨時變量
// 這里不能確定調(diào)用的方法是否會用到椖P穑空間來傳遞參數(shù)歇拆,所以這里暫不考慮鞋屈,fp == sp
sub sp, sp, #0x20
stp x29, x30, [sp]
mov x29, sp
// 緩存一些入?yún)ⅲ枰o下個方法騰出寄存器
// x9 = func
// x10 = x11 = argc
// x12 = x13 = args
mov x9, x0
mov x10, x1
mov x11, x1
mov x12, x2
mov x13, x2
// 沒有參數(shù)直接 CALL
cbz x11, DCCallFunc
// 第一個參數(shù)
ldr x0, [x12]
sub x11, x11, #1
cbz x11, DCCallFunc
// 第二個參數(shù)
ldr x1, [x12, #0x8]
sub x11, x11, #1
cbz x11, DCCallFunc
ldr x2, [x12, #0x10]
sub x11, x11, #1
cbz x11, DCCallFunc
ldr x3, [x12, #0x18]
sub x11, x11, #1
cbz x11, DCCallFunc
ldr x4, [x12, #0x20]
sub x11, x11, #1
cbz x11, DCCallFunc
ldr x5, [x12, #0x28]
sub x11, x11, #1
cbz x11, DCCallFunc
ldr x6, [x12, #0x30]
sub x11, x11, #1
cbz x11, DCCallFunc
// 第八個參數(shù)
ldr x7, [x12, #0x38]
sub x11, x11, #1
cbz x11, DCCallFunc
// 棧參數(shù)
// 開始計算椆拭伲空間厂庇,由于我們的參數(shù)都是int64類型
// 所以棧空間x15 = x11(剩余參數(shù)個數(shù)) * 8
mov x16, #8
mul x15, x11, x16
// The NSAA is rounded up to the larger of 8 or the Natural Alignment of the argument’s type
// 這里需要對齊输吏,我也不明白為什么
and x16, x15, #0x8
cbz x16, DCNoFixAlign
DCFixAlign:
add x15, x15, #0x8
DCNoFixAlign:
// 現(xiàn)在開始重新申請參數(shù)椚酰空間,并將椆峤Γ空間大小存入臨時變量`fp + 0x18`
DCStoreStackArgsLength:
str x15, [x29, #0x18]
sub sp, sp, x15
mov x15, sp
add x13, x12, #0x38
// 循環(huán)剩下的參數(shù)拄氯,逐個將其入棧:
// for arg in args+8:
// push(arg)
DCPushStackArgs:
add x13, x13, #0x8
ldr x14, [x13]
str x14, [x15]
sub x11, x11, #1
add x15, x15, #0x8
cbnz x11, DCPushStackArgs
// CALL
DCCallFunc:
blr x9
// 這里首先銷毀參數(shù)棧空間
ldr x15, [x29, #0x18]
cbz x15, DCRestoreStack
DCRestoreStackArgsLength:
add sp, sp, x15
// 然后還原fp, lr
// 銷毀當(dāng)前椝常空間
DCRestoreStack:
ldp x29, x30, [sp]
add sp, sp, #0x20
ret
// ReturnZero:
DCReturnZero:
mov x0, 0
ret
經(jīng)過測試译柏,可以看到所有參數(shù)都被正確的傳遞過去了,說明這種思路是正確的姐霍。
總結(jié)
那么C語言動態(tài)調(diào)用能給我們一些什么好處呢鄙麦。這是一種rpc的思想,而且這不需要額外的rpc支持镊折,就可以直接調(diào)用幾乎所有C方法胯府,但是這樣也給我們的程序帶來了一定的風(fēng)險,包括權(quán)限恨胚,參數(shù)類型等問題骂因。
同時也是快速實(shí)現(xiàn),或者說兼容C實(shí)現(xiàn)高級語言的一種方式赃泡,比如cpython就是利用了這種思想侣签。
由于不同平臺的差異性塘装,可能會導(dǎo)致兼容工作非常龐大,那么我們可以設(shè)計幾種類型的參數(shù)影所,或者固定幾個參數(shù)蹦肴,來簡化我們的兼容工作,比如將所有的對象都放到堆上猴娩,使用指針來傳遞阴幌。
開源項(xiàng)目libffi實(shí)現(xiàn)了多平臺的動態(tài)調(diào)用,有興趣的人可以自己去了解其實(shí)現(xiàn)卷中。