struct Point<'a> {
x: i32,
caption: &'a str,
y: i32,
}
static mut global_var: Point = Point {
x: 123,
y: 344,
caption: "123",
};
对应的LLVM IR为:
%Point = type
{
[0 x i64],
{ [0 x i8]*, i64 },
[0 x i32],
i32,
[0 x i32],
i32,
[0 x i32]
}
@_ZN5hello10global_var17h76c725a117a5fdc6E = internal global
<{ i8*, [16 x i8] }>
<{
i8* getelementptr inbounds
(
<{ [3 x i8] }>,
<{ [3 x i8] }>* @6,
i32 0,
i32 0,
i32 0
),
[16 x i8] c"\03\00\00\00\00\00\00\00{\00\00\00X\01\00\00"
}>,
align 8,
!dbg !330
我正在尝试寻找答案的两个有趣的点:
%Point
的类型定义中有空数组?这里好像不是Pascal风格的数组。global_var
以间接且特定于架构的方式初始化(整数的内容直接用小端缓冲区填充)?如果可能的话,我们能否以更易读的方式获得具有这些初始化的 LLVM IR 代码?
更新以回应一些评论:
如果我们记下字符串的十六进制表示,我们可以发现它正是 i64 3、i32 123 和 i23 344 在小端架构中的存储方式。
cargo rustc -- --emit=llvm-bc
生成,然后使用 llvm-dis
进行反汇编。免责声明:这个答案是有根据的猜测。
TL;DR:我猜显式填充数组和显式初始化是为了避免留下任何未初始化的字节,以及由此导致的未定义行为。
认识到 LLVM 继承了 C 的大部分低级语义,这一点很重要。毕竟,它的第一个也是最重要的前端是 Clang,而这塑造了它的大部分。
当 Clang 将
struct
降低到 LLVM IR 时,它会自信地让 LLVM 来计算填充link。
因此:
struct A
{
int a;
struct { char const* ptr; size_t len; } str;
char c;
};
A const GLOBAL{ 1, { "hello", 5 }, 'c' };
降低为:
%struct.A = type { i32, %struct.anon, i8 }
%struct.anon = type { i8*, i64 }
@_ZL6GLOBAL = internal constant %struct.A
{
i32 1,
%struct.anon
{
i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str, i32 0, i32 0),
i64 5
},
i8 99
}, align 8
@.str = private unnamed_addr constant [6 x i8] c"hello\00", align 1
这意味着填充字节未初始化,并且以典型的 C 方式读取未初始化的字节是未定义的行为。
这意味着使用未初始化的填充字节对结构体进行位复制是未定义的行为,虽然
memcpy
调用(降低为内在函数)似乎不受影响,但我不知道 C 标准中是否有任何提供 memcpy
的规定
通行证...
每当出现未定义行为时,Rust 都会采取强硬立场:
留下未初始化的填充字节,并让用户执行位复制,这看起来像是不必要的未定义行为来源:
似乎没有太多(如果有的话)性能优势:由于 Rust 可以自由地重新排列结构成员并压缩结构,因此通常很少有填充字节(只有几个尾随字节)。
因此,我的猜测是 rustc 显式指定填充数组2并显式初始化它们,以避免留下任何未初始化的填充字节。
1 仍然有。例如,由于 LLVM 考虑到如果值不适合,则将
float
转换为 int
是 UB,或者 LLVM 考虑到没有副作用的无限循环是 UB - 两者都是从 C 继承的。工作正在进行中。
2 这并没有提供 0 大小的数组的基本原理,这些对我来说似乎完全多余。