Почему Rust генерирует LLVM IR архитектурно-специфическим способом c и использует пустые массивы для изменяемых переменных stati c? - PullRequest
4 голосов
/ 24 января 2020
struct Point<'a> {
    x: i32,
    caption: &'a str,
    y: i32,
}

static mut global_var: Point = Point {
    x: 123,
    y: 344,
    caption: "123",
};

Соответствующий IR LLVM:

%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 в стиле.
  2. Почему global_var инициализируется косвенным образом и с учетом архитектуры c (содержимое целых чисел заполняется прямой порядок байтов), поскольку предполагается, что IR-код LLVM является архитектурно независимым?

Если возможно, можем ли мы получить IR-код LLVM с этими инициализациями в более читаемом стиле?

Обновление для ответа на некоторые комментарии:

  1. Почему c "\ 03 \ 00 \ 00 \ 00 \ 00 \ 00 \ 00 \ 00 {\ 00 \ 00 \ 00X \ 01 \ 00 \ 00 "это инициализация структуры?

Если мы запишем шестнадцатеричное представление строки, мы обнаружим, что именно i64 3, i32 123 и i23 344 хранятся в архитектуре с прямым порядком байтов.

Моя версия ржавчины - 1.41.0-nightly (19a0de242 2019-12-12). IR LLVM генерируется cargo rustc -- --emit=llvm-bc, а затем используется llvm-dis для разборки.

1 Ответ

2 голосов
/ 25 января 2020

Отказ от ответственности: этот ответ является обоснованным предположением.

TL; DR: я полагаю, что массивы явного заполнения и явная инициализация предназначены для того, чтобы избежать оставления любого неинициализированного байта, и неопределенное поведение, которое приводит к этому.


Обход через 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 (пониженные до внутренних) кажутся незатронутыми, я я не знаю ни одного положения в стандарте C, которое дает memcpy пропуск ...


Back to Rust

Rust занимает сильную позицию всякий раз, когда появляется неопределенное поведение :

  • В безопасном Rust не должно быть неопределенного поведения 1 .
  • В небезопасном ржавчине не должно быть неопределенного поведения.

Оставив позади неинициализированный па Добавление байтов и отключение пользователей, выполняющих битовое копирование, очень похоже на ненужный источник неопределенного поведения:

Кажется, что выигрыш в производительности невелик (если есть): Rust может свободно переставлять Члены структуры и структуры сжатия, как правило, содержат очень мало байтов заполнения (только несколько завершающих).

Поэтому я предполагаю, что rust c явно указывает массивы заполнения 2 и явно инициализирует их, чтобы избежать оставления любого неинициализированного байта заполнения.

1 Есть, все еще. Например, из-за того, что LLVM считает, что преобразование float в int является UB, если значение не подходит, или LLVM, учитывая, что бесконечное l oop без побочных эффектов является UB - оба унаследованы от C. Это незавершенная работа.

2 Это не дает обоснования для массивов нулевого размера, которые кажутся мне совершенно излишними.

...