为什么Rust以特定于架构的方式生成LLVM IR,并为可变静态变量使用空数组?

ngynwnxp  于 6个月前  发布在  其他
关注(0)|答案(1)|浏览(44)
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


有两个有趣的问题,我试图找到答案:
1.为什么在%Point的类型定义中有空数组?它在这里似乎不是Pascal风格的数组。
1.既然LLVM IR代码应该是架构独立的,为什么global_var要以间接和特定于架构的方式初始化(整数的内容直接填充一个小端缓冲区)?
如果可能的话,我们能以更可读的风格获得LLVM IR代码吗?
更新以回应一些评论:
1.为什么c”\03\00\00\00\00\00 {\00\00\00\00X\01\00\00”是结构的初始化?
如果我们写下字符串的十六进制表示,我们可以发现这正是i64 3,i32 123和i23 344在little-endian架构中的存储方式。
1.我的rust版本是1.41.0-nightly(19 a0 de 242 2019-12-12)。LLVM IR是由cargo rustc -- --emit=llvm-bc生成的,然后使用llvm-dis进行反汇编。

z9ju0rcb

z9ju0rcb1#

  • 免责声明:这个答案是一个有根据的猜测。
    TL;医生:我猜显式填充数组和显式初始化是为了避免留下任何未初始化的字节,以及由此导致的未定义行为。

C语言的迂回

重要的是要认识到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调用(被降低为intrinsic)似乎不受影响,但我不知道C标准中有任何条款给memcpy一个通行证。

回到Rust

每当出现Undefined Behavior时,Rust都会采取强硬的立场:

  • 安全Rust 1中不应该有未定义的行为。
  • 在不安全的Rust中应该有尽可能少的未定义行为。

留下未初始化的填充字节,并绊倒用户执行位复制,看起来非常像是一个不必要的未定义行为的来源:
似乎没有太多(如果有的话)性能好处:Rust可以自由地重新排列结构体成员,并压缩结构体,通常只有很少的填充字节(只有几个尾随字节)。
因此,我的猜测是rustc显式地指定了padding arrays 2并显式地将其删除,以避免留下任何未初始化的填充字节。
1* 仍然存在。例如,由于LLVM认为如果值不适合,则将float转换为int是UB,或者LLVM认为没有副作用的无限循环是UB -都是从C继承的。这是一项正在进行的工作。*
2* 这并没有为0大小的数组提供理由,这些对我来说似乎完全是多余的。*

相关问题