LINUX.ORG.RU

Если вам не хватало UB в C, то вам принесли ещё

 ,


2

5

Привет, мои дорогие любители сишки!

Если вам начало казаться, что разработчики стандарата языка C стали предсказуемыми и больше не могут удивлять вас новыми идеями, то вы ошибались. В новом стандарте C23, комитет постановил:

— zero-sized reallocations with realloc are undefined behavior;

То есть вот это валидный код:

void *ptr = malloc(0);
free(ptr);

А вот это – UB:

void *ptr = malloc(4096);
ptr = realloc(ptr, 0); <-- хаха UB

И это несмотря на то, что в манах уже давно написано следующее:

If size is equal to zero, and ptr is not NULL, then the call is equivalent to free(ptr)

Изменение вносится задним числом, наделяя кучу корректного (согласно документации glibc) кода способностью полностью изменить логику работы программы. Ведь это то, чего нам так не хватало!

В тред призываются известные эксперты по C: @Stanson и @alex1101, возможно они смогут нам объяснить, зачем разработчики стандарта C постоянно пытаются отстрелить себе обе ноги самыми нелепыми способами.



Последнее исправление: cumvillain (всего исправлений: 1)
Ответ на: комментарий от cumvillain

Ладно, возможно я был не прав. Но какие-то костыли для системных хедеров там точно были. Возможно, для -std=c11 они и не нужны. Но pedantic тут не факт что поможет, т.к. там могут быть исключения прописаны чтоб много чего в этих хедерах не действовало.

firkax ★★★★★
()
Ответ на: комментарий от cumvillain

Так. Оптимизации вокруг UB возможны как раз при наличии unsafe без UB. Чтение элементов массива без проверок на длину этого массива, например. Или, в случае LRU, выделение/освобождение памяти в unsafe. Если бы доступ по освобождённому указателю не был UB, его надо было бы при каждом чтении как-то проверять или гарантировать, что доступа к освобождённому никогда не будет.

monk ★★★★★
()
Ответ на: комментарий от monk

Так. Оптимизации вокруг UB возможны как раз при наличии unsafe без UB. Чтение элементов массива без проверок на длину этого массива, например.

А тебе не кажется что это уже следствие? Компилятор не делает вокруг этого оптимизаций, и выигрыш мы получаем потому что в unsafe этих проверок просто нет изначально.

Или, в случае LRU, выделение/освобождение памяти в unsafe. Если бы доступ по освобождённому указателю не был UB, его надо было бы при каждом чтении как-то проверять или гарантировать, что доступа к освобождённому никогда не будет.

Я как-то разделяю эти две вещи:

  • Я испортил память и теперь программа невалидная
  • Компилятор встретил в коде UB, и на основании этого, сделал неправильные выводы

То есть это вроде бы два разных класса проблем. В одной ты сам накосячил (и, учитывая unsafe, знаешь где), во втором компилятор сделал неверные выводы на основании твоего кода.

cumvillain
() автор топика
Ответ на: комментарий от crutch_master

Тормознутое говнище по производительности находится где-то между жабкой и lua с пистоном. Обнови мантры.

anonymous
()
Ответ на: удаленный комментарий

По-моему это ты не выкупил. Это же рофл был.

anonymous
()
Ответ на: комментарий от anonymous

из твоего монитора вылезет бородатый мужик и отшлёпает тебя по твоей маленькой жопке

неплохие фантазии, я, кста, не против

Да ты и бородатой собаки из монитора не против…

LamerOk ★★★★★
()
Ответ на: комментарий от cumvillain

То есть это вроде бы два разных класса проблем. В одной ты сам накосячил (и, учитывая unsafe, знаешь где), во втором компилятор сделал неверные выводы на основании твоего кода.

В случае Rust unsafe «Undefined behavior affects the entire program.». Если в Java/Python через FFI можно испортить память VM, то в Rust unsafe позволяет передать в LLVM код, который будет оптимизироваться также как C/C++ с выкидыванием «ненужных» блоков кода (даже если эти блоки не в unsafe).

monk ★★★★★
()
Ответ на: комментарий от cumvillain

Компилятор не делает вокруг этого оптимизаций, и выигрыш мы получаем потому что в unsafe этих проверок просто нет изначально.

На первом этапе - да. А на втором компилятор может убрать недоступный код на основании условий UB или заменить на более быстрый алгоритм. Например, for(int i=a; i!=b; i++) сразу выкидывать, если b меньше, чем a и внутри цикла нет побочных эффектов.

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от cumvillain

Возможно кто-то из-за этого даже умрет

Давно пора!

// Тред не читал.

mord0d ★★★★★
()
Ответ на: комментарий от monk

то в Rust unsafe позволяет передать в LLVM код, который будет оптимизироваться также как C/C++ с выкидыванием «ненужных» блоков кода (даже если эти блоки не в unsafe).

Не понимаю откуда ты это берешь. Порча памяти и use after free аффектят всю программу вне зависимости от оптимизаций.

cumvillain
() автор топика
Ответ на: комментарий от monk

сразу выкидывать, если b меньше, чем a и внутри цикла нет побочных эффектов

Да, но причем тут ub? Вижу проверка инвариантов, но не вижу влияние ub.

cumvillain
() автор топика
Ответ на: комментарий от cumvillain

Не понимаю

Патамушта тупой

anonymous
()

Ядро Linux на Си и UB в нём нет.
Многие популярные проекты на Си и тоже всё Ok!

«Призрак UB ходит по ЛОР и никак не уймётся».

Forum0888
()
Ответ на: комментарий от Forum0888

Ядро Linux на Си и UB в нём нет.

аллилуйя во истину так!

anonymous
()
Ответ на: комментарий от cumvillain

Да, но причем тут ub? Вижу проверка инвариантов, но не вижу влияние ub.

ub гарантирует, что i++ > i.

monk ★★★★★
()
Ответ на: комментарий от monk

Ты хочешь сказать отсутствие UB? Ну так это сишная проблема, в том же хрусте переполнение int’а ни разу не UB. И даже в C с нужными флагами это не UB.

cumvillain
() автор топика
Ответ на: комментарий от cumvillain

Опять растодрочеры припёрлись в топик про сишечку.

anonymous
()
Ответ на: комментарий от cumvillain

Порча памяти и use after free аффектят всю программу вне зависимости от оптимизаций.

Да. Но Rust можно заставить вести себя как Си.

if cond() 
{  
  do1();
} else { 
  do2();
  let x = &mut 42;
  let xptr = x as *mut i32;
  let x1 = unsafe { &mut *xptr };
  let x2 = unsafe { &mut *xptr };
  *x1 = 0;
}

При оптимизации может скомпилироваться в безусловный вызов do1().

А в Java аналогичный код в любом случае может испортить память только после вызова do2().

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от Forum0888

Ядро Linux на Си и UB в нём нет.

Linux разваливается в хлам если не останавливать clang в попытках оптимизировать код.

Именно поэтому Linux собирается с

-fno-strict-aliasing
-fno-delete-null-pointer-checks
-fno-allow-store-data-races
-fno-strict-overflow

https://github.com/torvalds/linux/blob/6098d87eaf31f48153c984e2adadf14762520a87/Makefile#L560

https://github.com/torvalds/linux/blob/6098d87eaf31f48153c984e2adadf14762520a87/Makefile#L809

https://github.com/torvalds/linux/blob/6098d87eaf31f48153c984e2adadf14762520a87/Makefile#L824

https://github.com/torvalds/linux/blob/6098d87eaf31f48153c984e2adadf14762520a87/Makefile#L993z

Как минимум эти 4 флага, это оптимизации, которые отключены, потому что Linux разваливается если их включить и разрабы Linux знают об этом и просто не способны избавиться от UB в их коде.

А с этими флагами и примеры UB из этой ветки работают ожидаемо :)

fsb4000 ★★★★★
()
Последнее исправление: fsb4000 (всего исправлений: 2)
Ответ на: комментарий от cumvillain

Поэтому Rust не может выкидывать такой цикл, а должен выполнять его до переполнения и затем достижения i==b.

monk ★★★★★
()
Ответ на: комментарий от fsb4000

Нашёл коммит, когда добавили -fno-delete-null-pointer-checks

https://github.com/torvalds/linux/commit/a3ca86aea507904148870946d599e07a340b39bf

Пример, просто фейспалм.

Вот код,

static void __devexit agnx_pci_remove(struct pci_dev *pdev)
{
    struct agnx_priv *priv = dev->priv;
    if (!dev)
       return;
    // что-то дальше, неважно
}

Очевидно, что использование dev раньше теста на NULL, поэтому компилятор выкинул

if (!dev)
   return;

Они решили что это не секьюрно, так как добавляет дыры в ядро. А так, если отключить оптимизации, то надеются что их пронесёт и хаков будет меньше.

Turning on this flag could prevent the compiler from optimising away some "useless" checks for null pointers.  Such bugs can sometimes become exploitable at compile time because of the -O2 optimisation.


Clearly the 'fix' is to stop using dev before it is tested, but building with -fno-delete-null-pointer-checks flag at least makes it harder to abuse.

Просто пример для @Forum0888 , а то будет верить в святых разработчиков ядра, которые пишут без UB.

fsb4000 ★★★★★
()
Последнее исправление: fsb4000 (всего исправлений: 1)
Ответ на: комментарий от monk

И вместо этого выведет. Ебе ворнинг, если может доказать, что ты ерунду делаешь. Ворнинг явно лучше.

cumvillain
() автор топика
Ответ на: комментарий от monk

При оптимизации может скомпилироваться в безусловный вызов do1().

Есть воспроизводимый пример?

anonymous
()
Ответ на: комментарий от anonymous

Вот, например: https://godbolt.org/z/8br67vzbE

fn test(num: i32) -> bool {
    num < 10
}

fn square(num: i32) -> i32 {
    if test(num) {
        0
    } else {
        // BUG: causes UB in safe code if `test()` returns `false`
        unsafe { std::hint::unreachable_unchecked() }
    }
}

Скомпилируется в

square:
        xor     eax, eax
        ret

И никакого сюрприза в этом нет. UB оно и в расте UB

red75prim ★★★
()
Последнее исправление: red75prim (всего исправлений: 3)
Ответ на: комментарий от Forum0888

В примере же есть. Вот, разыменование NULL указателя https://github.com/torvalds/linux/commit/6bf676723ab2a6ade05feb2cd0b5073930f72d7e

А если ты просишь непофикшенного UB, то я не разработчик ядра, это нужно аудит делать и много времени тратить. Но UB есть потому что ядро без выключенных оптимизаций падает, это факт. А почему оно падает, это нужно искать и фиксить.

fsb4000 ★★★★★
()
Ответ на: комментарий от fsb4000
Ответ на: комментарий от Forum0888

Потому что Линус ориентировался только на gcc, а разработчики gcc ориентировались на ядро Линукса. В смысле, старались не сломать компиляцию ядра.

По стандарту оно UB, но разработчики gcc решили доопределить эти UB так, чтобы ядро компилировалось нормально. И они имеют на это полное право. Но это означает, что с другими компиляторами работа ядра не гарантирована.

red75prim ★★★
()
Последнее исправление: red75prim (всего исправлений: 1)
Ответ на: комментарий от monk

Почему в Rust смогли, а в C – нет?

В Rust свои UB есть:https://doc.rust-lang.org/reference/behavior-considered-undefined.html

В C и Rust разные трактовки термина «Undefined Behaviour». В Rust это означает «если ты обосрался с указателями, сам виноват, мы ничего не гарантируем». В C это означает «мы изнасилуем твой код и ты получишь чудовищный треш и адов холокост».

Проще говоря, Rust не делает диких трансформаций кода с расчётом на это самое UB или его отсутствие, как в примерах про C выше.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от cumvillain

Текст программы взять с годболта (для поста я выкинул неважные детали вроде pub и #[no_mangle]), записать в test.rs и

rustc --crate-type lib --emit asm -O test.rs

Результат в test.s

red75prim ★★★
()
Последнее исправление: red75prim (всего исправлений: 1)
Ответ на: комментарий от red75prim

Видимо зависит от того, как main выглядит. В одном случае не генерируется main, во втором компилятор действительно выкинул test.

cumvillain
() автор топика
Ответ на: комментарий от hateyoufeel

Проще говоря, Rust не делает диких трансформаций кода с расчётом на это самое UB или его отсутствие, как в примерах про C выше.

Пример же привели для Rust.

Трансформации делает LLVM и они абсолютно те же самые, что и для Си.

monk ★★★★★
()
Ответ на: комментарий от hateyoufeel

Вот программа с опечаткой, которая не выводит никаких предупреждений, а просто выкидывает «лишний код»: https://godbolt.org/z/rx79Kv46Y

#[inline(never)]
pub fn search(num: i32, arr: [i32; 10]) -> i32 {
    // returns 1 iff num in arr, else 0
    for i in 0..=10 {
        unsafe {
            if *arr.get_unchecked(i) == num {
                return 1
            }
        }
    }
    0
}

pub fn main () {
    let arr: [i32; 10] = [1,2,3,4,5,6,7,8,9,10];
    print!("{}", search(42, arr));
    print!("{}", search(69, arr));
}

Выведет два раза 1, несмотря на то, что в массиве нет ни 42 ни 69 и чтение arr[10] не может одновременно равняться и тому и другому.

monk ★★★★★
()
Ответ на: комментарий от monk

Опечатка тут правда на 3 строки, так как

    for i in arr {
        if i == num {
            return 1
        }
    }

отработает не медленнее, чем индексирование с get_unchecked.

red75prim ★★★
()
Ответ на: комментарий от red75prim

Разумеется. Это просто пример того, что любой код с unsafe может внезапно спровоцировать UB в самом неожиданном месте.

А unsafe там очень часто встречается.

monk ★★★★★
()
Ответ на: комментарий от red75prim

Попробуй переписать без get_unchecked вот такое:

pub fn search_odd(num: i32, arr: [i32; 10]) -> i32 {
    // returns 1 iff num in odd pos of arr, else 0
    for i in 0..=5 {
        unsafe {
            if *arr.get_unchecked(i*2-1) == num {
                return 1
            }
        }
    }
    0
}
monk ★★★★★
()
Ответ на: комментарий от monk

Где «там»? У меня в рабочем проекте unsafe, в общем-то, только в FFI. Вызовы функций из сишных библиотек для оптимизатора непрозрачны, так что особого потенциала для неожиданных оптимизаций там нет. В стандартной библиотеке каждый unsafe уже раз по 5 раз перепроверен. В сторонних библиотеках, которые я использую, unsafe’ов тоже не так уж и много.

red75prim ★★★
()
Ответ на: комментарий от red75prim

Где «там»?

Среднестатистически, в коде, написанном на Rust.

У меня в рабочем проекте unsafe, в общем-то, только в FFI.

В конечной программе на Си++ обычно тоже ни прямой работы с памятью ни переполнения знаковой арифметики.

В сторонних библиотеках, которые я использую, unsafe’ов тоже не так уж и много.

Ладно. Моё «очень часто» и твоё «не так уж и много» вполне может оказаться одним и тем же числом. Оценка субъективна.

monk ★★★★★
()
Ответ на: комментарий от monk
    for &i in arr.iter().skip(1).step_by(2)
    {
        if i == num {
            return 1
        }
    }

Но тут код получается менее эффективным. Оптимизатор не разворачивает цикл.

Можно сделать так и получить результат идентичный get_unchecked

    for &i in
        arr.iter()
        .enumerate()
        .filter(|(idx, _)| idx%2 == 1)
        .map(|(_, i)| i)

https://godbolt.org/z/ansbofKrj

red75prim ★★★
()
Ответ на: комментарий от red75prim

Можно сделать так и получить результат идентичный get_unchecked

Круто. На Си++ функциональные цепочки компилятором не раскручиваются.

monk ★★★★★
()
Ответ на: комментарий от red75prim

Вызовы функций из сишных библиотек для оптимизатора непрозрачны, так что особого потенциала для неожиданных оптимизаций там нет.

Смотря как связывать.

Если через

clang ./clib.c -flto=thin -c -o ./clib.o -O2
# Create a static library from the C code
ar crus ./libxyz.a ./clib.o

# Invoke `rustc` with the additional arguments
rustc -Clinker-plugin-lto -L. -Copt-level=2 -Clinker=clang -Clink-arg=-fuse-ld=lld ./main.rs

то вроде прозрачны.

monk ★★★★★
()
Ответ на: комментарий от monk

У меня 100% тулчейнов используют gcc, и библиотеки из используемых SDK собраны без -flto, то есть GIMPLE там нет и оптимизатор курит.

red75prim ★★★
()

Не, ну кто просил-то, а?

sparkie ★★★★★
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.