从 C 库调用符号时忽略 return 值是否安全
Is it safe to ignore return values when calling symbols from a C library
我一直在摆弄 LLVM 并编写了一个简单的编译器。它使用 libc 作为标准库。当然,我必须以某种方式在我的 IR 中声明函数。
我注意到以下似乎有效:
declare void @puts(i8*)
在 C 中,函数定义如下:
int puts(const char *s);
所以它真的应该是
declare i32 @puts(i8*)
这是一个非常简单的案例,但我确信我在过程中的某个地方会在声明这些函数时出错。例如,在阅读联机帮助页之前,我并不知道 puts
返回了一个 int。
这些错误有多严重?它会弄乱堆栈还是 LLVM 会以某种方式处理它?此类错误的安全隐患是什么?
注意:我无法使用 puts
的 void
声明产生任何错误。
小 return 值通常放在 return 值寄存器中,因此忽略这些值不会致命崩溃。对于较大的值,某些 ABI 要求调用者分配堆栈 space 并将其作为不可见的第一个参数传递给函数,在这种情况下,您的程序可能会很快崩溃,因为您不会分配或传递它。如果您使用的是不存储前一帧指针的 abi,即它必须知道它自己的栈帧有多大并且abi允许被调用者调整栈指针,这也是致命的。
基本上它可能会起作用,直到它不起作用。
这个问题的答案取决于您的 C 编译器的 ABI 使用的调用约定。在 x86 和 x86-64 上大多数 C 编译器使用的约定中,return 值在寄存器中传递。将 int
-returning 函数错误声明为 void
将导致 return 寄存器的值被忽略(如果您不使用它)。这不会造成任何伤害,因为调用者无论如何都要负责保存 eax
寄存器。
例如下面的代码:
void callee(int, int, int);
void caller(void)
{
callee(1, 2, 3);
}
...如果您将 callee
声明为 return int
而不是 void
.
,将被编译成完全相同的程序集
这适用于 "small" return 类型,即由整数、双精度浮点数或 64 位整数组成的类型(其中 x86 return s 在两个整数寄存器中)。大型 return 类型的处理方式不同 - 如果您将 callee
的声明更改为:
struct { char x[100]; } callee(int, int, int);
...调用代码将发生巨大变化,尽管传入的类型没有改变。 return 结构现在将分配到调用者的堆栈上,其地址将作为隐藏的第一个参数传递给被调用者(这是在 x86 上,在 x86-64 上情况略有不同),预计将 return 值写入该区域。
换句话说,只要您了解调用约定,并且注意不要错误声明 return 大值类型的函数(AFAIK 在标准 C 和POSIX 个库),错误的声明将起作用。
理查德
到目前为止的答案都很好,但我认为一个重要的含义是,如果您忽略 C 函数 return,作为其功能的一部分,分配内存或 open/create 文件等.等等,然后 return 某种指针。
当然,忽略这些将孤立只有在程序退出时才会释放的内存(如果它能做到这一点),让文件保持打开状态,等等。
基本上,如果您调用的函数 return 是任何东西 但是 寄存器值或堆栈实例值,其含义可能很重要。
我一直在摆弄 LLVM 并编写了一个简单的编译器。它使用 libc 作为标准库。当然,我必须以某种方式在我的 IR 中声明函数。
我注意到以下似乎有效:
declare void @puts(i8*)
在 C 中,函数定义如下:
int puts(const char *s);
所以它真的应该是
declare i32 @puts(i8*)
这是一个非常简单的案例,但我确信我在过程中的某个地方会在声明这些函数时出错。例如,在阅读联机帮助页之前,我并不知道 puts
返回了一个 int。
这些错误有多严重?它会弄乱堆栈还是 LLVM 会以某种方式处理它?此类错误的安全隐患是什么?
注意:我无法使用 puts
的 void
声明产生任何错误。
小 return 值通常放在 return 值寄存器中,因此忽略这些值不会致命崩溃。对于较大的值,某些 ABI 要求调用者分配堆栈 space 并将其作为不可见的第一个参数传递给函数,在这种情况下,您的程序可能会很快崩溃,因为您不会分配或传递它。如果您使用的是不存储前一帧指针的 abi,即它必须知道它自己的栈帧有多大并且abi允许被调用者调整栈指针,这也是致命的。
基本上它可能会起作用,直到它不起作用。
这个问题的答案取决于您的 C 编译器的 ABI 使用的调用约定。在 x86 和 x86-64 上大多数 C 编译器使用的约定中,return 值在寄存器中传递。将 int
-returning 函数错误声明为 void
将导致 return 寄存器的值被忽略(如果您不使用它)。这不会造成任何伤害,因为调用者无论如何都要负责保存 eax
寄存器。
例如下面的代码:
void callee(int, int, int);
void caller(void)
{
callee(1, 2, 3);
}
...如果您将 callee
声明为 return int
而不是 void
.
这适用于 "small" return 类型,即由整数、双精度浮点数或 64 位整数组成的类型(其中 x86 return s 在两个整数寄存器中)。大型 return 类型的处理方式不同 - 如果您将 callee
的声明更改为:
struct { char x[100]; } callee(int, int, int);
...调用代码将发生巨大变化,尽管传入的类型没有改变。 return 结构现在将分配到调用者的堆栈上,其地址将作为隐藏的第一个参数传递给被调用者(这是在 x86 上,在 x86-64 上情况略有不同),预计将 return 值写入该区域。
换句话说,只要您了解调用约定,并且注意不要错误声明 return 大值类型的函数(AFAIK 在标准 C 和POSIX 个库),错误的声明将起作用。
理查德
到目前为止的答案都很好,但我认为一个重要的含义是,如果您忽略 C 函数 return,作为其功能的一部分,分配内存或 open/create 文件等.等等,然后 return 某种指针。
当然,忽略这些将孤立只有在程序退出时才会释放的内存(如果它能做到这一点),让文件保持打开状态,等等。
基本上,如果您调用的函数 return 是任何东西 但是 寄存器值或堆栈实例值,其含义可能很重要。