相信以后还会有III,IIII,V……
类型转换是有代价的
最近在写虚拟机解释引擎的时候遇到的这个问题。Java虚拟机中虽然有多种数据类型,但是实际上在Java栈上,只有两种类型,占用一个栈槽位的和占用两个栈槽位的,也就是4字节与8字节。所以一开始设计Java栈的API的时候使用了两套接口:
unsinged int Stack_pop4(T);
void Stack_push4(T, unsinged int);
unsigned long long Stack_pop8(T);
void Stack_push8(T, unsinged long long);
这里的问题是,将float类型push之后,当再次pop出来时,结果就不正确了。
C语言中float占4个字节,无符号整形数也是4个字节,这样如果有4个字节的数据放在内存中,具体它是什么值取决于我们把它当成什么类型去解释。这一点非常类似数据与结构的关系,数据往往只有一份,但是可以用多种结构来表示,结构只是数据的不同视图罢了。内存中的数据当然也是这样,放在那里都是没有类型的字节,就看CPU如何去解释它们。
但是将同样的数据用不同的类型来解释,在C语言中不能用类型转换来实现,原因在于类型转换不是免费的!
loat x = 3.14;
unsigned int y = (unsigned int)x;
float z = (float)y; // z已经不是3.14了
之所以说类型转换不是免费的,因为这里可以将它理解为一种运算:unsigned int trans_f2i(float)
.并且这种运算并不是拿着原始数据生成一个新的数据的视图,而是复制一份数据并对数据做修改。显然y与x在内存中的地址是不一样的。
使用指针可以将任意合法内存当做任意类型的读写。
float x = 3.14;
unsigned int *p = &x;
unsigned int y = *p;
float *f = &y;
float z = *f; // z = 3.14
灵活使用union也可以获得同样的效果。
union {
unsigned int u;
float f;
}u;
float x = 3.14;
u.f = x;
unsigned int y = u.u;
float z = u.f; // z = 3.14
将任意合法内存作为参数传给已定义的函数
需求看上去有点奇怪,不过如果真的写起Runtime类的程序,这种需求恐怕还是挺多的。可以理解为函数都已经定义好了,但是函数的参数可能是通过网络传来的,或者通过其他过程计算得到的一块内存区域。比如通过dlopen,dlsym可以找到函数指针,不过如何用一个通用模块给找到的函数传递参数呢?
合理的利用程序调用栈的布局似乎是一个好主意。这篇文章里面详细介绍了栈相关的细节(貌似链接挂了,以后我会自己写一篇放上)。比如如下这段程序:
int foo(int x, int y, int z);
struct Args {
unsigned data[MAX];
};
int main()
{
int(*f)();
f = foo;
struct Args args;
int *body = (int*)args.data;
body[0] = 1;
body[1] = 2;
body[2] = 3;
f(args);
}
利用结构体值传递的特点,可以将一块内存作为参数传给接受任意个任意类型参数的函数。当然这里的方法很多,用不定长数组总是分配在栈的低地址的特点也可以达到同样的效果,就不在举例子了。
让我掉进陷阱的是这套方案在64bit的系统上不能用了……最后发现其实就是ABI的问题。在64bit上gcc应该是默认用寄存器传参,而32bit都是默认用栈传参。看到也有人有同样的需求x86-64-forcing-gcc-to-pass-arguments-on-the-stack。我没有查相关的ABI文档,不过做了点实验,反编译了一些代码,64bit上应该前6个参数都是通过寄存器传递的,多于6个的参数通过栈传递。当然这个不是很准确,权威答案还需要查看相应的文档。我只是用这个例子测了一下:
struct args {
long long data[64];
};
int foo(int x1, int x2, int x3
, int x4, int x5, int x6
, int x7, int x8, int x9){
printf("hello, world %d %d %d\n", x7, x8, x9);
return 1;
}
typedef int(*fTy)();
int main()
{
fTy f;
f = foo;
struct args Args;
Args.data[0] = 100;
Args.data[1] = 200;
Args.data[2] = 300;
f(1,2,3,4,5,6,Args); // print hello, world 100 200 300
return 0;
}
当然这种方法本身就是一种hack,是破坏ABI的一种做法。即使在32bit上也是不能保证正确性的,因为32bit上可以通过优化利用2个寄存器传参,只是默认关闭而已。
网友评论