libffi淺析
最近調試了weston的一個coredump,對libffi有了一些了解,在此記錄下,使用的是arm處理器,32位,soft float,libffi3.1,使用的abi是SYSV。
libffi簡介和使用示例:https://www.atmark-techno.com/~yashi/libffi.html,建議先看完,有所了解再繼續看本文。大體意思就是libffi用於高級語言之間的相互調用。由於函數指針,參數類型,參數個數,參數的值都可以在運行時指定,所以在腳本語言調用c裏麵用的比較多,比如python 的ctypes;也可以調用不同abi(應用程序二進製接口)編譯的程序,這個了解的不多。
數據類型
libffi定義了ffi_type結構體,用於描述對應的c語言中的uint32, sint32, floate, void *, struct等類型:
typedef struct _ffi_type { size_t size; unsigned short alignment; unsigned short type; struct _ffi_type **elements; } ffi_type;比如變量ffi_type_uint32用於描述c語言的uint32類型,它所占大小是4;對齊大小是4;在libffi中用於標記類型的數字是FFI_TYPE_UINT32,也就是9;elements在c語言基本類型中沒有用到,固定為NULL,elements在結構體中才會用到,為結構體中的元素。
ffi_type_uint32變量是通過FFI_TYPEDEF(uint32, UINT32, FFI_TYPE_UINT32)得到的
#define FFI_TYPEDEF(name, type, id) \ struct struct_align_##name { \ char c; \ type x; \ }; \ const ffi_type ffi_type_##name = { \ sizeof(type), \ offsetof(struct struct_align_##name, x), \ id, NULL \ } #define FFI_NONCONST_TYPEDEF(name, type, id) \ struct struct_align_##name { \ char c; \ type x; \ }; \ ffi_type ffi_type_##name = { \ sizeof(type), \ offsetof(struct struct_align_##name, x), \ id, NULL \ }定義了struct_align_uint32結構體,這一係列結構體的第一個元素都是char,第二個是具體的uint32,sint32,void *等,用於之後求取對齊字節數。
ffi_type_uint32為ffi_type類型的const變量,sizeof(uint32)得到uint32的大小;offsetof類似於內核裏麵著名的container_of函數中求取結構體中元素偏移字節數的代碼,可以得到uint32在struct_align_uint32中的偏移為4,表示uint32是4字節對齊的;id是FFI_TYPE_UINT32,值為9;elements為NULL。
函數調用
有了類型,下麵就看函數調用,分為兩步:
一、初始化ffi_cif結構體
ffi_cif結構體定義為:
typedef struct { ffi_abi abi; unsigned nargs; ffi_type **arg_types; ffi_type *rtype; unsigned bytes; unsigned flags; #ifdef FFI_EXTRA_CIF_FIELDS FFI_EXTRA_CIF_FIELDS; #endif } ffi_cif;表示了函數調用中的一些信息,比如abi;輸入參數個數;輸入參數類型(ffi_type_uint32之類的);返回值類型;輸入參數占用空間的大小(aapcs要求進入arm函數時堆棧是8字節對齊的。由於這個緩衝區是在sysv.S的ffi_call_SYSV函數中通過sub sp, fp, r2申請的,申請完就調用ffi_prep_args_SYSV,所以這個大小必須是8的倍數);flags(如果返回類型是c語言基本類型,那麼flags就是返回類型,如果返回類型是結構體,需要有所處理,見ffi_prep_cif_machdep函數)。
使用如下函數初始化ffi_cif結構體:
ffi_status FFI_HIDDEN ffi_prep_cif_core(ffi_cif *cif, ffi_abi abi, unsigned int isvariadic, unsigned int nfixedargs, unsigned int ntotalargs, ffi_type *rtype, ffi_type **atypes) { unsigned bytes = 0; unsigned int i; ffi_type **ptr; FFI_ASSERT(cif != NULL); FFI_ASSERT((!isvariadic) || (nfixedargs >= 1)); FFI_ASSERT(nfixedargs <= ntotalargs); if (! (abi > FFI_FIRST_ABI && abi < FFI_LAST_ABI)) return FFI_BAD_ABI; cif->abi = abi; cif->arg_types = atypes; cif->nargs = ntotalargs; cif->rtype = rtype; cif->flags = 0; #if HAVE_LONG_DOUBLE_VARIANT ffi_prep_types (abi); #endif /* Initialize the return type if necessary */ if ((cif->rtype->size == 0) && (initialize_aggregate(cif->rtype) != FFI_OK)) return FFI_BAD_TYPEDEF; /* Perform a sanity check on the return type */ FFI_ASSERT_VALID_TYPE(cif->rtype); /* x86, x86-64 and s390 stack space allocation is handled in prep_machdep. */ #if !defined M68K && !defined X86_ANY && !defined S390 && !defined PA /* Make space for the return structure pointer */ if (cif->rtype->type == FFI_TYPE_STRUCT #ifdef SPARC && (cif->abi != FFI_V9 || cif->rtype->size > 32) #endif #ifdef TILE && (cif->rtype->size > 10 * FFI_SIZEOF_ARG) #endif #ifdef XTENSA && (cif->rtype->size > 16) #endif #ifdef NIOS2 && (cif->rtype->size > 8) #endif ) bytes = STACK_ARG_SIZE(sizeof(void*)); #endif for (ptr = cif->arg_types, i = cif->nargs; i > 0; i--, ptr++) { /* Initialize any uninitialized aggregate type definitions */ if (((*ptr)->size == 0) && (initialize_aggregate((*ptr)) != FFI_OK)) return FFI_BAD_TYPEDEF; /* Perform a sanity check on the argument type, do this check after the initialization. */ FFI_ASSERT_VALID_TYPE(*ptr); #if !defined X86_ANY && !defined S390 && !defined PA #ifdef SPARC if (((*ptr)->type == FFI_TYPE_STRUCT && ((*ptr)->size > 16 || cif->abi != FFI_V9)) || ((*ptr)->type == FFI_TYPE_LONGDOUBLE && cif->abi != FFI_V9)) bytes += sizeof(void*); else #endif { /* Add any padding if necessary */ if (((*ptr)->alignment - 1) & bytes) bytes = (unsigned)ALIGN(bytes, (*ptr)->alignment); #ifdef TILE if (bytes < 10 * FFI_SIZEOF_ARG && bytes + STACK_ARG_SIZE((*ptr)->size) > 10 * FFI_SIZEOF_ARG) { /* An argument is never split between the 10 parameter registers and the stack. */ bytes = 10 * FFI_SIZEOF_ARG; } #endif #ifdef XTENSA if (bytes <= 6*4 && bytes + STACK_ARG_SIZE((*ptr)->size) > 6*4) bytes = 6*4; #endif bytes += STACK_ARG_SIZE((*ptr)->size); } #endif } cif->bytes = bytes; /* Perform machine dependent cif processing */ #ifdef FFI_TARGET_SPECIFIC_VARIADIC if (isvariadic) return ffi_prep_cif_machdep_var(cif, nfixedargs, ntotalargs); #endif return ffi_prep_cif_machdep(cif); } #endif /* not __CRIS__ */ ffi_status ffi_prep_cif(ffi_cif *cif, ffi_abi abi, unsigned int nargs, ffi_type *rtype, ffi_type **atypes) { return ffi_prep_cif_core(cif, abi, 0, nargs, nargs, rtype, atypes); }
需要詳細說明下sysv的傳參方式:
1、輸入參數通過r0-r3傳遞,多餘的放入堆棧中;返回值放入r0,不夠的話放入{r0,r1}或者{r0,r1,r2,r3},比如:
int foo(int a, int b, int c, int d), 輸入:r0 = a, r1 = b, r2 = c, r3 = d,返回:r0 = 類型為int的retvalue
int *foo(char a, double b, int c, char d), 輸入:r0 = a, r1用於對齊(double 要求8字節對齊), b = {r2, r3},c放在堆棧的sp[0]位置,d放在堆棧的sp[4]位置,這裏的sp是指進入函數時的sp;返回:r0 = 類型為int *的retvalue
2、注意如果返回值是結構體,情況有些特殊:
struct client foo(int a, char b, float c), 輸入:r0 = 一個strcut client *變量,由調用者給出, r1 = a, r2 = b, r3 = c;返回:strcut client *變量,和調用者給的一樣
bytes大小的計算:
1、如果返回值是結構體,一個結構體指針需要傳遞給函數,因此bytes+=4 (sizeof(struct xxx *) = 4)
2、如果bytes的大小不滿足參數的對齊要求,比如bytes=5時,下一個需要處理的輸入參數是double(size=8, align=8),那麼bytes向上取align=8的倍數,所以bytes=8
3、將參數放入緩衝區(bytes就是緩衝區的大小,緩衝區在ffi_call_SYSV中申請的)時,如果參數size<sizeof(int),那麼就按照int的大小來存放(注意有無符號),因為即使是傳遞一個char,也得使用一個獨立的寄存器,一個寄存器不能傳遞兩個char參數
具體將參數放入緩衝區的,由ffi_prep_args_SYSV函數處理:
int ffi_prep_args_SYSV(char *stack, extended_cif *ecif, float *vfp_space) { register unsigned int i; register void **p_argv; register char *argp; register ffi_type **p_arg; argp = stack; if ( ecif->cif->flags == FFI_TYPE_STRUCT ) { *(void **) argp = ecif->rvalue; argp += 4; } p_argv = ecif->avalue; for (i = ecif->cif->nargs, p_arg = ecif->cif->arg_types; (i != 0); i--, p_arg++, p_argv++) { argp = ffi_align(p_arg, argp); argp += ffi_put_arg(p_arg, p_argv, argp); } return 0; }
二、調用函數指針fn
將準備ffi_cif和調用fn分開的原因是,函數可能需要使用不同的參數值調用多次,但是參數類型是不變的。
通過如下代碼,可以進行函數調用:
/* Prototypes for assembly functions, in sysv.S */ extern void ffi_call_SYSV (void (*fn)(void), extended_cif *, unsigned, unsigned, unsigned *); extern void ffi_call_VFP (void (*fn)(void), extended_cif *, unsigned, unsigned, unsigned *); void ffi_call(ffi_cif *cif, void (*fn)(void), void *rvalue, void **avalue) { extended_cif ecif; int small_struct = (cif->flags == FFI_TYPE_INT && cif->rtype->type == FFI_TYPE_STRUCT); int vfp_struct = (cif->flags == FFI_TYPE_STRUCT_VFP_FLOAT || cif->flags == FFI_TYPE_STRUCT_VFP_DOUBLE); unsigned int temp; ecif.cif = cif; ecif.avalue = avalue; /* If the return value is a struct and we don't have a return */ /* value address then we need to make one */ if ((rvalue == NULL) && (cif->flags == FFI_TYPE_STRUCT)) { ecif.rvalue = alloca(cif->rtype->size); } else if (small_struct) ecif.rvalue = &temp; else if (vfp_struct) { /* Largest case is double x 4. */ ecif.rvalue = alloca(32); } else ecif.rvalue = rvalue; switch (cif->abi) { case FFI_SYSV: ffi_call_SYSV (fn, &ecif, cif->bytes, cif->flags, ecif.rvalue); break; case FFI_VFP: #ifdef __ARM_EABI__ ffi_call_VFP (fn, &ecif, cif->bytes, cif->flags, ecif.rvalue); break; #endif default: FFI_ASSERT(0); break; } if (small_struct) { FFI_ASSERT(rvalue != NULL); memcpy (rvalue, &temp, cif->rtype->size); } else if (vfp_struct) { FFI_ASSERT(rvalue != NULL); memcpy (rvalue, ecif.rvalue, cif->rtype->size); } }
cif是剛才準備好的那個;fn是將要調用的函數指針;rvalue用於存放fn的返回值,與rtype對應;avalue用於存放fn的輸入參數的值,與arg_types對應。
ffi_call的核心是ffi_call_SYSV函數,這個一個匯編函數,主要意思是:
ARM_FUNC_START(ffi_call_SYSV) @ 函數開頭保存了幾個寄存器,lr是調用者的pc指針 @ Save registers stmfd sp!, {r0-r3, fp, lr} UNWIND .save {r0-r3, fp, lr} @ 備份sp指針 mov fp, sp UNWIND .setfp fp, sp @ 通過減sp,申請內存,大小為bytes @ 因為申請內存後,按照aapcs的要求,調用ffi_prep_args_SYSV時sp需要是8的倍數,所以bytes也必須是8的倍數 @ Make room for all of the new args. sub sp, fp, r2 @ ffi_prep_args_SYSV是根據arg_types和avalue,將bytes大小的數據放入堆棧裏,r0和r1是它的輸入參數 @ r0是緩存的起始地址,r1是ecif,ecif包含了cif,rvalue,avalue @ Place all of the ffi_prep_args in position mov r0, sp @ r1 already set @ Call ffi_prep_args(stack, &ecif) bl CNAME(ffi_prep_args_SYSV) @ 經過ffi_prep_args_SYSV的處理,fn所需要的參數已經都放在堆棧裏了 @ 前16字節的參數放到r0~r3寄存器裏,如果是4個int,那麼r0~r3分別存放fn從左到右第1個到第4個參數 @ 如果是char, double這樣的,由於對齊的要求,{r2,r3}存放double,char在r0的低字節中,r1無用 @ r0~r3如果沒有保存完fn所有的參數,那麼其他參數放在堆棧中 @ 比如有6個int參數,那麼第5個int就在fn函數一開始的sp[0]位置,第6個在sp[4] @ move first 4 parameters in registers ldmia sp, {r0-r3} @ 按照上麵說的放參數的規則,調整好sp的位置 @ and adjust stack sub lr, fp, sp @ cif->bytes == fp - sp ldr ip, [fp] @ load fn() in advance cmp lr, #16 movhs lr, #16 add sp, sp, lr @ r0~r3存放前4個參數,sp指向第5個參數,調用fn @ call (fn) (...) call_reg(ip) @ 恢複sp @ Remove the space we pushed for the args mov sp, fp @ r2用來保存fn的返回值 @ Load r2 with the pointer to storage for the return value ldr r2, [sp, #24] @ r3 = flags,flags根據rtype返回類型設置的 @ Load r3 with the return type code ldr r3, [sp, #12] @ 如果rvalue == NULL,不保存返回值,退出函數 @ 如果不為NULL,那麼根據rtype的不同,按照不同的方式保存返回值 @ If the return value pointer is NULL, assume no return value. cmp r2, #0 beq LSYM(Lepilogue) @ return INT cmp r3, #FFI_TYPE_INT #if defined(__SOFTFP__) || defined(__ARM_EABI__) cmpne r3, #FFI_TYPE_FLOAT #endif streq r0, [r2] beq LSYM(Lepilogue) ...... LSYM(Lepilogue): #if defined (__INTERWORKING__) ldmia sp!, {r0-r3,fp, lr} bx lr #else ldmia sp!, {r0-r3,fp, pc} #endif .ffi_call_SYSV_end: UNWIND .fnend #ifdef __ELF__ .size CNAME(ffi_call_SYSV),.ffi_call_SYSV_end-CNAME(ffi_call_SYSV)
最後更新:2017-10-26 17:34:21