libffi浅析

最近调试了weston的一个coredump,对libffi有了一些了解,在此记录下,使用的是arm处理器,32位,soft float,libffi3.1,使用的abi是SYSV。

 

libffi简介和使用示例:http://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)

 

时间: 2025-01-21 00:18:59

libffi浅析的相关文章

linux进程调度浅析

操作系统要实现多进程,进程调度必不可少. 有人说,进程调度是操作系统中最为重要的一个部分.我觉得这种说法说得太绝对了一点,就像很多人动辄就说"某某函数比某某函数效率高XX倍"一样,脱离了实际环境,这些结论是比较片面的. 而进程调度究竟有多重要呢? 首先,我们需要明确一点:进程调度是对TASK_RUNNING状态的进程进行调度(参见<linux进程状态浅析>).如果进程不可执行(正在睡眠或其他),那么它跟进程调度没多大关系. 所以,如果你的系统负载非常低,盼星星盼月亮才出现一

浅析win7下IE8主页被篡改的修复过程

浅析win7下IE8主页被篡改的修复过程 很多网友都有这个烦恼,在打开住页面时,页面就会开始变化,不再是自己熟悉的版本主页,所以要解决IE8被篡改的问题,我们就要充分的挖掘Windows7的系统"潜能",提升让IE8自我保护能力.现在我们深度xp系统下载一起来看看要怎么解决吧! 运行注册表编辑器,一次展开到HKEY_CURRENT_USER/Software/Policies/Microsoft,在此分支下新建一个名为"ControlPanel"的项,(具体操作为:

linux pi_futex浅析

Priority Inheritance,优先级继承,是解决优先级反转的一种办法. 一个经典的例子:A/B/C三个实时进程,优先级A>B>C.C持有a锁,而A等待a锁被挂起.原本C释放a锁之后,A进程就可以继续执行的,但是偏偏有个比C优先级高的B进程存在,导致C得不到运行,也就没法释放a锁,从而导致A进程一直挂起.从整体上看,进程B虽然比A优先级低,但它却成功的抢占掉了A.这就是所谓的优先级反转. 一种解决办法是优先级继承,C在持有a锁期间临时继承等待者A的优先级,那么B进程就无法从中捣乱了.

linux内核SMP负载均衡浅析

需求 在<linux进程调度浅析>一文中提到,在SMP(对称多处理器)环境下,每个CPU对应一个run_queue(可执行队列).如果一个进程处于TASK_RUNNING状态(可执行状态),则它会被加入到其中一个run_queue(且同一时刻仅会被加入到一个run_queue),以便让调度程序安排它在这个run_queue对应的CPU上面运行. 一个CPU对应一个run_queue这样的设计,其好处是: 1.一个持续处于TASK_RUNNING状态的进程总是趋于在同一个CPU上面运行(其间,这

linux文件读写浅析

在<linux内核虚拟文件系统浅析>这篇文章中,我们看到文件是如何被打开.文件的读写是如何被触发的. 对一个已打开的文件fd进行read/write系统调用时,内核中该文件所对应的file结构的f_op->read/f_op->write被调用. 本文将顺着这条路走下去,大致看看普通磁盘文件的读写是怎样实现的. linux内核响应一个块设备文件读写的层次结构如图(摘自ULK3): 1.VFS,虚拟文件系统. 之前我们已经看到f_op->read/f_op->write如

linux进程状态浅析

众所周知,现在的分时操作系统能够在一个CPU上运行多个程序,让这些程序表面上看起来是在同时运行的.linux就是这样的一个操作系统. 在linux系统中,每个被运行的程序实例对应一个或多个进程.linux内核需要对这些进程进行管理,以使它们在系统中"同时"运行.linux内核对进程的这种管理分两个方面:进程状态管理,和进程调度.本文主要介绍进程状态管理,进程调度见<linux进程调度浅析>. 进程状态 在linux下,通过ps命令我们能够查看到系统中存在的进程,以及它们的状

linux网络报文接收发送浅析

对于linux内核来说,网络报文由网络设备来进行接收.设备驱动程序从网络设备中读取报文,通过内核提供的网络接口函数,将报文传递到内核中的网络协议栈.报文经过协议栈的处理,或转发.或丢弃.或被传送给某个进程. 网络报文的发送与之相反,进程通过系统调用将数据送入网络协议栈,或者由网络协议栈自己发起报文的发送,然后协议栈通过调用网络接口函数来调度驱动程序,使其将报文传送给网络设备,从而发送出去. 本文讨论的是网络接口层,它是网络设备驱动程序与网络协议栈交互的纽带.见下图中红色部分的netif. 报文的

浅析VC与Matlab联合编程(三)

在"浅析VC与Matlab联合编程<一>"和"浅析VC与Matlab联合编程<二>"中介绍了matcom,这个工具可以将用matlab写的m文件翻译成C++文件,或者是可执行文件(exe)或库文件(dll).但是matcom在很多方面也有限制,比如,对struct等类的支持有缺陷,部分绘图语句无法实现或得不到准确图象,尤其是三维图象. 实际上VC与matlab的接口实现方法有很多种,matcom只是其中一种,本文再介绍一种比较容易实现的方法:

PHP中的流(streams)浅析

  这篇文章主要介绍了PHP中的流(streams)浅析,本文讲解了流的概述.流基础知识.php://包装器.流上下文(Stream Contexts)等内容,需要的朋友可以参考下 概述 流(streams)是PHP4.3版本引入的一个特性,主要是为了统一文件.sockets以及其他类似资源的工作方法.PHP4.3距今已经有很长时间了,但是很多程序员似乎都不能正确使用PHP中的流,当然这也包括我.以前也在一些程序中遇到过流的使用,如php://input,但是一直没机会整理,今天就把这部分知识整