当我将手册页读到dlopen()时,我无意中发现了这段代码:

cosine = (double (*)(double)) dlsym(handle, "cos");

           /* According to the ISO C standard, casting between function
              pointers and 'void *', as done above, produces undefined results.
              POSIX.1-2001 and POSIX.1-2008 accepted this state of affairs and
              proposed the following workaround:

                  *(void **) (&cosine) = dlsym(handle, "cos");

              This (clumsy) cast conforms with the ISO C standard and will
              avoid any compiler warnings.

              The 2013 Technical Corrigendum 1 to POSIX.1-2008 improved matters
              by requiring that conforming implementations support casting
              'void *' to a function pointer.  Nevertheless, some compilers
              (e.g., gcc with the '-pedantic' option) may complain about the
              cast used in this program. */

我知道,将函数指针强制转换为空指针,反之亦然,这是未定义的行为.该标准之所以将其设置为未定义的行为,是因为体系 struct 的差异,其中函数指针可能与数据指针的大小不同,或者在某些情况下,函数指针实际上由两个值表示(至少我听说过).我理解解决办法如何避免未定义的行为,因为将cosine的地址转换为void **实际上只是将指向函数的指针的数据指针转换为void **,这是完全有效的,当然,它也完全有效地取消对void **的引用,并将dlsym()返回的void *赋给它.然而,如果存在前面提到的体系 struct 怪癖,这段代码难道不会像将函数转换为空指针一样容易出错吗?如果是这样的话,该标准不是也应该指定此解决方法也是未定义的行为吗?这进一步引出了这样一个问题:是否可以从一开始就实现dlsym()功能的不容易出错的实现?

推荐答案

*(void **) (&cosine) = dlsym(handle, "cos");

This (clumsy) cast conforms with the ISO C standard and will
avoid any compiler warnings.

不,它不是"顺从"的.具体地说,它不是严格一致的代码,其行为也不是由C标准定义的.

从前面的代码推断,cosine被定义为double (*)(double),这是一个指向接受double并返回double的函数的指针.上面的代码使用类型void *的左值对其进行写入.这违反了C 2018 6.5 7中的别名规则.该段说明用于访问的有效类型和类型的组合是由C标准定义的,而访问带有void *的函数的指针不在其中.

此外,C标准不要求double (*)(double)void *具有相同的大小或相同的表示形式,因此从理论上讲,写入字节可能会完全扰乱指针.(然而,从粗心大意的程序员的Angular 来看,这比利用别名规则进行优化会使程序混乱的编译器要少得多.)

一种修复方法是创建一个返回指向任何函数类型的指针的dlsymf routine ,因为C标准定义了在指向不同函数类型的指针之间进行转换的行为(只要实际调用使用了合适的类型).

我知道,将函数指针强制转换为空指针,反之亦然,这是未定义的行为.该标准将其设置为未定义行为的理由是由于体系 struct 差异,其中函数指针可能与数据指针的大小不同,或者在某些情况下,函数指针实际上由两个值表示(至少我听说过)

这不是原因.两种类型的大小不同这一事实并不妨碍它们之间的转换,我们可以很容易地将int转换为charlong long intdouble,这些类型的大小通常与int不同.允许转换执行计算以产生其结果.(转换实际上是这样一种操作:在一种类型中获取一个值,并在可行的情况下在另一种类型中生成相同的值.它不仅采用操作数字节来表示新类型的值.)该标准还要求在void *和任何指向对象的指针类型之间进行转换,前提是满足对齐要求,但这些指针也可以具有不同的大小.

我不特别知道真正的原因是什么.对于一些C实现来说,这可能被认为是繁重的,因为它们具有分离的数据和指令空间以及执行转换的不寻常的寻址方案,而且要求它们这样做几乎没有好处.但这并不是因为指针的大小.

在任何情况下,解决方案都很简单:定义将void *,特别是dlsym返回的void *转换为指向函数类型的指针的行为.实际上,任何支持POSIX的C实现都必须这样做,这样dlsym才能工作.C标准说这是未定义的行为,但这并不意味着我们必须让它保持未定义状态.该标准对"未定义的行为"的含义仅仅是该标准没有强加任何要求.它不要求我们将其保持为未定义;我们可以添加我们自己对它所做的事情的定义,然后将为我们的C实现定义行为.

事实上,C 2018 J.5.7 1指出这是一个常见的扩展:

指向对象或指向void的指针可以转换为指向函数的指针,从而允许将数据作为函数调用(6.5.4).

C++相关问答推荐

通过MQTT/蚊子发送大文件—限制在4MB

Malloc(sizeof(char[Length]))是否不正确?

为什么在C中进行大量的位移位?

__VA_OPT__(,)是否可以检测后面没有任何内容的尾随逗号?

C编译器是否遵循restrict的正式定义?

如何将字符串传递给函数并返回在C中更改的相同字符串?

在C++中使用函数指针的正确语法

C中函数类型的前向声明

在为hashmap创建加载器时,我的存储桶指向它自己

在txt文件中找到指定的字符串,并从数字中减go 相同的值

致命错误:ASM/rwan ce.h:没有这样的文件或目录.符号链接还不够

在vfork()之后,链接器如何在不 destruct 父内存的情况下解析execve()?

为什么编译器不能简单地将数据从EDI转移到EAX?

不带Malloc的链表

在文件描述符上设置FD_CLOEXEC与将其传递给POSIX_SPOWN_FILE_ACTIONS_ADCLOSE有区别吗?

在Ubuntu上使用库部署C程序的最佳实践

C: NULL>;NULL总是false?

x86-64平台上的int_fast8_t大小与int_fast16_t大小

为什么孤儿进程在 Linux 中没有被 PID 1 采用,就像我读过的一本书中声称的那样?

如何正确探测平台设备?