确定可变参数函数中的参数大小
Determining argument size in variadic functions
我正在为我的玩具 OS 内核实现一个 printk
函数,目标是 x86 平台。如果我这样调用 printk
:
uint64_t x = 0xdead;
uint64_t z = 0xbeef;
printk("%p %s\n", x & z, "yes");
即传递一个64位整数(x & z
)给它,则生成的汇编代码为:
c01000bd: c7 45 f0 ad de 00 00 movl [=11=]xdead,-0x10(%ebp)
c01000c4: c7 45 f4 00 00 00 00 movl [=11=]x0,-0xc(%ebp)
c01000cb: c7 45 e8 ef be 00 00 movl [=11=]xbeef,-0x18(%ebp)
c01000d2: c7 45 ec 00 00 00 00 movl [=11=]x0,-0x14(%ebp)
c01000d9: 8b 45 f0 mov -0x10(%ebp),%eax
c01000dc: 23 45 e8 and -0x18(%ebp),%eax
c01000df: 89 c3 mov %eax,%ebx
c01000e1: 8b 45 f4 mov -0xc(%ebp),%eax
c01000e4: 23 45 ec and -0x14(%ebp),%eax
c01000e7: 89 c6 mov %eax,%esi
c01000e9: 68 e4 17 10 c0 push [=11=]xc01017e4
c01000ee: 56 push %esi
c01000ef: 53 push %ebx
c01000f0: 68 e8 17 10 c0 push [=11=]xc01017e8
c01000f5: e8 27 0a 00 00 call c0100b21 <printk>
你可以看到这里gcc使用了两个32位的寄存器(%esi
和%ebx
)来存储64位的值。这样做的结果是压入堆栈的参数数量变成了 4 个而不是 3 个。如果 printk
中的代码无法计算出参数的大小并正确使用它,堆栈访问就会被搞乱。
所以我的问题是,在实现可变参数函数时,使用 va_arg
宏时如何知道下一个参数的大小?或者具体来说,如何解决这个 32 位与 64 位 printk
问题?
C 不提供确定参数大小或类型的通用方法。对于 printf
之类的函数,您必须依赖格式字符串。这就是为什么与传递的参数相比格式字符串错误会导致一些非常错误的代码;因为如果你弄错了尺寸,你可能会读到预期的参数。格式字符串还告诉您参数的数量。
还需要考虑的另一件事是可变参数函数的默认参数提升。您可以在 this SO question.
中找到更多相关信息
这里是一个示例,说明如果格式字符串与您通过参数传递的类型不匹配,会发生什么。你需要在 32 位机器上编译这个或者使用 gcc -m32 file.c
:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("%u: %s\n", (1ULL << 63), "Hello World");
return 0;
}
那为什么这么糟糕,看起来还不错,我们有 2 个格式字符和 2 个参数,对吗?没那么快,对于 32 位机器来说 %u
是 32 位,字符指针也是。但是 (1ULL << 63) 是 64 位长。所以发生的是将 96 个字节压入堆栈(或者传递参数)。尽管格式字符串只使用前 64 位。前 32 位全为零,而后 32 位(用于 char 指针的位)的值为 (1 << 31) .由于 printf
需要一个 char 指针,该值被取消引用,这会导致未定义的行为,特别是我机器上的分段错误。
您必须使用其中一个参数来确定大小(通常是第一个参数)。使用 printf() 时,大小由格式字符串中的格式说明符决定。这并没有什么神奇之处。
我正在为我的玩具 OS 内核实现一个 printk
函数,目标是 x86 平台。如果我这样调用 printk
:
uint64_t x = 0xdead;
uint64_t z = 0xbeef;
printk("%p %s\n", x & z, "yes");
即传递一个64位整数(x & z
)给它,则生成的汇编代码为:
c01000bd: c7 45 f0 ad de 00 00 movl [=11=]xdead,-0x10(%ebp)
c01000c4: c7 45 f4 00 00 00 00 movl [=11=]x0,-0xc(%ebp)
c01000cb: c7 45 e8 ef be 00 00 movl [=11=]xbeef,-0x18(%ebp)
c01000d2: c7 45 ec 00 00 00 00 movl [=11=]x0,-0x14(%ebp)
c01000d9: 8b 45 f0 mov -0x10(%ebp),%eax
c01000dc: 23 45 e8 and -0x18(%ebp),%eax
c01000df: 89 c3 mov %eax,%ebx
c01000e1: 8b 45 f4 mov -0xc(%ebp),%eax
c01000e4: 23 45 ec and -0x14(%ebp),%eax
c01000e7: 89 c6 mov %eax,%esi
c01000e9: 68 e4 17 10 c0 push [=11=]xc01017e4
c01000ee: 56 push %esi
c01000ef: 53 push %ebx
c01000f0: 68 e8 17 10 c0 push [=11=]xc01017e8
c01000f5: e8 27 0a 00 00 call c0100b21 <printk>
你可以看到这里gcc使用了两个32位的寄存器(%esi
和%ebx
)来存储64位的值。这样做的结果是压入堆栈的参数数量变成了 4 个而不是 3 个。如果 printk
中的代码无法计算出参数的大小并正确使用它,堆栈访问就会被搞乱。
所以我的问题是,在实现可变参数函数时,使用 va_arg
宏时如何知道下一个参数的大小?或者具体来说,如何解决这个 32 位与 64 位 printk
问题?
C 不提供确定参数大小或类型的通用方法。对于 printf
之类的函数,您必须依赖格式字符串。这就是为什么与传递的参数相比格式字符串错误会导致一些非常错误的代码;因为如果你弄错了尺寸,你可能会读到预期的参数。格式字符串还告诉您参数的数量。
还需要考虑的另一件事是可变参数函数的默认参数提升。您可以在 this SO question.
中找到更多相关信息这里是一个示例,说明如果格式字符串与您通过参数传递的类型不匹配,会发生什么。你需要在 32 位机器上编译这个或者使用 gcc -m32 file.c
:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("%u: %s\n", (1ULL << 63), "Hello World");
return 0;
}
那为什么这么糟糕,看起来还不错,我们有 2 个格式字符和 2 个参数,对吗?没那么快,对于 32 位机器来说 %u
是 32 位,字符指针也是。但是 (1ULL << 63) 是 64 位长。所以发生的是将 96 个字节压入堆栈(或者传递参数)。尽管格式字符串只使用前 64 位。前 32 位全为零,而后 32 位(用于 char 指针的位)的值为 (1 << 31) .由于 printf
需要一个 char 指针,该值被取消引用,这会导致未定义的行为,特别是我机器上的分段错误。
您必须使用其中一个参数来确定大小(通常是第一个参数)。使用 printf() 时,大小由格式字符串中的格式说明符决定。这并没有什么神奇之处。