C语言陷阱II

作者: qc1iu | 来源:发表于2018-05-03 14:05 被阅读73次

相信以后还会有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个寄存器传参,只是默认关闭而已。

相关文章

  • C语言陷阱II

    相信以后还会有III,IIII,V…… 类型转换是有代价的 最近在写虚拟机解释引擎的时候遇到的这个问题。Java虚...

  • C语言陷阱

    最近一直做C编译器相关的开发,感觉该总结一下。以前一直以为对C已经足够熟悉了,结果被它奇葩的语法树震惊了。碰巧最近...

  • C语言陷阱

    1、当我们使用 printf 打印字符串时,要用 printf("%s", s ); 而不能用 printf( s...

  • C语言陷阱「词法陷阱 之字符与字符串」

    C语言陷阱【词法陷阱 之字符与字符串】 字符与字符串 C语言中的单引号' ',与双引号" ",含义不同。 用单引号...

  • 书籍推荐

    1.《C primer plus》----《C程序设计语言》-----《C和指针》、《C专家编程》、《C缺陷与陷阱...

  • go+i18n实现多语言切换

    go+ii8n实现多语言切换 包go get github.com/syyongx/ii18n 配置文件目录--c...

  • Redis 数据类型

    Redis Redis 学习笔记 II Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、...

  • Java中的断言assert

    Java陷阱之assert关键字 一、概述 在C和C++语言中都有assert关键,表示断言。在Java中,同样也...

  • 有效使用C语言的建议

    如何有效地使用C语言? 避免C语言的陷阱,不要依赖编译器来检测代码中的问题。 使用工具来改进您的程序,尤其是lin...

  • C语言中的词法陷阱

    该文章为笔记,因此许多内容摘抄自《C陷阱与缺陷》。《C陷阱与缺陷》,全书不厚,但是感觉十分有提醒与启迪作用,值得阅...

网友评论

    本文标题:C语言陷阱II

    本文链接:https://www.haomeiwen.com/subject/ijwkrftx.html