Pull to refresh

Comments 29

А на AVR не пробовали? У меня получилось.

        for _ in 0..50_000 {
            unsafe { core::arch::asm!("nop") };
        }

Вот интересно, что было бы если бы подобное было написано d основном цикле на C? А так да - великий и могучий Rust.

И еще вопрос.

pub unsafe fn inb(port: u16) -> u8

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

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

нет. оно снимает часть ограничений, которые присутвуют в safe коде.

  • Dereference a raw pointer

  • Call an unsafe function or method

  • Access or modify a mutable static variable

  • Implement an unsafe trait

  • Access fields of a union

src

... компилятор решит выкинуть повторное чтение из одного и того же порта или переставит местами две операции ввода-вывода

Интересно... А вариант использовать unsafe вокруг "core::arch::asm!("in al, dx", out("al") value, in("dx") port);" как это сделано в main() вокруг "nop" не прокатит? Компилятор просто выбросит не проверяя что именно функция делает?

И что же это получается - "великий и могучий" использует худшие практики SUN'овского компилятора - выбрасывать и переставлять ассемблерные вставки пользователя если ему это специально не запретить? А за одно не избавляет от избыточно умных оптимизаций кода (главная проблема на-Си-льников во встраиваемых системах)?

Ничего подобного - unsafe {} только разрешает выполнять вещи, которыми потенциально можно устроить UB, а unsafe fn обозначает, что у функции есть какие-то не проверяемые компилятором требования, которые для вызова функции нужно соблюсти, и следовательно вызывать её нужно в блоке unsafe. Никакого дополнительного связанного с переупорядочиванием и прочим изменением семантики кода у unsafe поведения нет.

Из примечательного - unsafe fn имеет внутри себя неявный unsafe {} блок, но в последних версиях компилятора отсутствие явного unsafe {} кидает предупреждение.

#![allow(dead_code)]

pub unsafe fn inb(port: u16) -> u8 {
    let value: u8;
    core::arch::asm!("in al, dx", out("al") value, in("dx") port);
    value
}

т.е. unsafe fn здесь реально бесполезна, как я писал в первом комментарии, как и блок unsafe вокруг asm? А опасения @Deosis абсолютно беспочвенны?

Блок unsafe {} вокруг asm! как раз нужен, поскольку это одна из тех вещей, которой потенциально можно вызвать UB, и вне блока unsafe использовать asm! просто не получится.

Учитывая, что тут нет никаких вариантов вызова UB с помощью данной функции, то unsafe fn вполне могла бы быть обычной функцией.

По сути принято и понято. По факту мне сложно представить как "nop" или "in" на это способны. Если in, хоть и с большим трудом и аппаратными особенностями, но можно притянуть, то nop... Я просто хочу понять логику автора кода. И да, я крайне плохо знаю Rust. Потому часть вопросов задаю с позиции привычных мне языков (С/asm).

Так "nop" на это и не способен, но содержимое asm! в общем случае вполне может что-то не то сделать, если его не так написать. Поскольку компилятор не может отличить первый случай от второго (да и невозможно благодаря проблеме останова), unsafe {} необходимо ставить при любом использовании asm!. И известно безопасные использования asm! оборачивать в безопасные функции (почему в данном случае автор этого не сделал для inb - для меня загадка).

unsafe не даёт таких гарантий. Он только для человека что-то значит, не для компилятора.

вызов asm вставок всегда оборачивается в unsafe блок.

Это ритуал такой? Типа обязательной утренней молитвы?

Или тотальное недоверие программисту по определению не умеющему safe на ассемблере?

И зачем тогда требовать его явно указывать, если это реально ВСЕГДА так?

Потому что можно потом погрепать "unsafe" и найти все места, где UB вообще имеет шанс случиться. Поскольку за пределами этого блока UB не должно быть возможно по определению. Говорят, что при ревью довольно удобно.

Компилятор особо ничего не знает касательно конкретных вариантов asm, поэтому провалидировать, что не происходит какого-то подозрительного поведения он достоверно не может. Поэтому asm вставка оборачивается в unsafe и считается, что программист знает, что он сделал все корректно в пределах этого блока : память выделена и выровнена, данные какие-то имеются, указатели указывают на рабочую память, а не куда-то за пределы или в освобождённую память. То есть наоборот, в рамках unsafe блока происходит полное (ну, почти) доверие программисту. А тот кто вызывает этот код не обязан доверять тому программисту и поэтому было бы неплохо сказать, что вот тут может быть опасненько, поэтому вызовы unsafe функций и оборачиваются в unsafe блок. Если есть уверенность в корректности блока, то можно сделать safe обёртку над этим. В контексте кода автора - скорее всего так просто исторически сложилось.

Знаете, я уже третий раз пытаюсь ответ написать. Так чтоб ненароком не обидеть тонкие чувства. Но:

  1. Это не ответ на заданный вопрос. Если любой asm! автоматически считается небезопасным, то зачем это указывать явно в коде?

  2. Использовать у себя код, которому нет доверия? Вы серьезно?

  3. Если у обертки asm! не стоит unsafe, то по той же логике мы вполне доверяем. И чего спрашивается? Или это работает как-то по другому?

Впрочем, ну его. И так скорее всего из банальной опечатки столько текста наплодили.

Тем более, что главная-то претензия не здесь была. Она была к нарезки времени пустым циклом. Очень зависимо от конкретного ПК и даже его настроек энергосбережения. В свое время над Digger'ом ржали. Там такое решение было, в итоге даже на 386 он носился как наскипедаренный. А именно эту часть пропустили.

Впрочем оно тоже понятно - библиотеки с задержкой нет. А значит тут "путь самурая".

Использовать у себя код, которому нет доверия? Вы серьезно?

В С например в документации к memcpy описано несколько вариантов, когда может случиться UB. В контексте этого обсуждения, это и есть тот самый недоверенный код. Но ведь все используют memcpy! Просто нельзя нарушать её инварианты.

В Rust такие функции помечают как unsafe, и в документации также как и в С описано, как её вызывать правильно, т.к. компилятор не может это проверить сам (а в случае asm! он вообще ничего проверить не может). А компилятор не даст просто так вызвать unsafe функцию.

Через unsafe блок программист говорит компилятору: я добавил в код все необходимые проверки (которые описаны в документации), и гарантирую что функция вызвана правильно. Ровно то же самое нужно делать на С с опасными функциями, но реализуется это не компилятором, а статическими анализаторами, санитайзерами и т.д.

Если же в unsafe блоке в коде ошибка, то может произойти UB с теми же последствиями, что и в С. Но тут программист хотя бы знает где начать искать ошибку.

Спасибо, стало понятнее

На Rust пишут полубоги и боги - они читают документацию на все, и все без исключения. А на unsafe функции с особой тщательностью. А на C быдлокодеры, которые документацию не читают и не пишут в принципе. Вообще и никогда. И отладке не обучены - потому где программа ломается они не знают.

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

Теперь буду знать, что unsafe - это не для компилятора, а для программиста. Указание на то, что надо посмотреть документацию или реализацию.

Ладно, подождем когда оно в мой мир доползет. Там и будем разбираться серьезнее. Вообще, очень хотелось бы посмотреть на ассемблерный код данного примера в режиме параллельного перевода. Как это бы транслировалось с С я понимаю. Интересно чем будет отличаться кодогенерация с Rust.

Теперь буду знать, что unsafe - это не для компилятора, а для программиста. Указание на то, что надо посмотреть документацию или реализацию.

unsafe - это контракт между программистом и компилятором. Если все его придерживаются, то он полностью избавляет от многих классов UB. На практике все ошибаются, и шанс не равен нулю, но очень мал.

Вообще я удивляюсь, что все так прицепились к unsafe блокам, как будто видят их впервые. В C# например они были еще с версии 1.0, задолго до Rust. И никого не смущало, что нужно оборачивать опасные места в коде в блоки unsafe.

Если любой asm! автоматически считается небезопасным, то зачем это указывать явно в коде?

Всё, что не может быть проанализировано статически и может потенциально нарушать инварианты безопасного кода обязано быть помечено unsafe. asm! это некоторый макрос (что-то типа #define в Си, только чище), который неявно генерирует unsafe функцию. unsafe функция может быть вызвана исключительно в unsafe контексте - блоке или другой функции, отмеченные этим ключевым словом. Всё это сделано намеренно для того, чтобы люди пытались писать именно безопасный код, не прибегая к чёрной магии, которую нельзя проверить никак иначе, кроме как глазами или тестированием. Это гарантирует, что никаких double-free/use-after-free или разыменования nullptr вне этих блоков произойти не может.

Использовать у себя код, которому нет доверия? Вы серьезно?

Не знаю, почему вас удивляет, что чужой код может содержать segfault или делать грязь при помощи UB.

Если у обертки asm! не стоит unsafe, то по той же логике мы вполне доверяем

По вышеописанным причинам если мы просто бахнем asm! посреди безопасного кода, то оно банально не соберётся с ошибкой "нельзя вызывать вне unsafe блока".

Для остального есть crust

А именно эту часть пропустили

ну в чем сакральный смысл nop в цикле не разбирался, но у вас была ещё и вторая часть с вопросом про "зачем там unsafe". Про него собственно и отвечал.

#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    let mut game = Arkanoid::new();

Ага. Осмотрелся повнимательнее. Я правильно понимаю, что это заклинание как раз сделало то, что категорически не приветствуется. Объявило unsafe контекст на весь код. И дальше не столь важно будет или не будет вложенный блок (функция) помечена как unsafe - это уже ничего не изменит. А данный пример, делает по сути не больше и не безопаснее, чем аналогичный С код? Так получается?

Тем более интересно посмотреть подстрочную трансляцию. И в первую очередь третьей строчки в этом частичном листинге.

И еще - а что реально такая большая разница между asm! в Rust и asm{} в С? Я полагал что это куда как более близкие родственники, чем #define.

Потом - не всякий ассемблер небезопасен. В частности примеры автора - надо сильно много думать, чтоб придумать их небезопасное поведение. nop как инструкция абсолютно безопасна - она просто не делает ничего (кроме инкремента PC). Чтение порта... Ну, черт знает. Была бы запись - да, возможно. Чтение... На вскидку даже не предположу что и как оно может поломать. Правда и на 100% уверенно сказать что не может не возьмусь. Но на 99,9 точно уверен - не может. Если к кого есть пример как оно может поломать - пишите. Будет интересно заняться цифровой археологией. Раз уж все равно клавиатуру из 0x60-ого порта считываем (это даже не юность - это мое детство).

Собственно потому и удивляет безусловное недоверие ассемблерным вставкам. Фобией попахивает. Или параноей. Кому что ближе.

#[unsafe(no_mangle)] просто выключает name mangling для данной функции. Никакого unsafe контекста на весь код тут нет.

Трансляцию можно посмотреть с помощью какого-нибудь cargo-asm, но, если честно, не совсем уверен, что вы хотите там увидеть - при сборке с оптимизацией вызова Arkanoid::new() там просто нет.

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

Трансляцию можно посмотреть с помощью какого-нибудь cargo-asm, но, если честно, не совсем уверен, что вы хотите там увидеть - при сборке с оптимизацией вызова Arkanoid::new() там просто нет.

Ну давайте так - этот проект одна из множества попыток поиграть на embedded поле. Без runtime, без аллокатора, и даже без ОС. И в отличии от обычных Skeleton-ов тут есть какой-то ощутимый результат. Тем и интересен. Потому и пошел смотреть, а не пролистал как обычно.

Да, есть "золотой фонд" - змейка, тетрис, арканоид. Ну еще с пламя, бублик и ряд других находок с демосцены. Достаточно простые в реализации, не требовательные к ресурсам. Все они в некотором смысле "Hello, World!". Тем и интересны.

Только вот по embedded правилам игра идет несколько по шуллерски (или по читерски если хотите). В этом нет ничего плохого, но просто надо понимать - тогда это не Embedded, а скорее Demo. Что в обзем случае тоже совсем не плохо. Да, здесь нет ОС. Но кто-то должен проинициализировать память, накопители, прочитать BOOT сектор, передать управление. Т.е. подготовить поляну для работы кода. В реальной жизни embedded проекта этого всего, как правило, нет и этим озадачен сам проект. Тут этим всем занимается BIOS. Ну да бог с ней - это не страшно, а скорее полезно. Хотя, чего тут - тот же код, но подменяющий не BOOT сектор, а образ. BIOS (раз уж он такой независимый) - это было бы сильно интереснее и показательнее и куда как ближе к тому самому embedded (хотя, безусловно, на порядок сложнее).

В частности здесь я без ассемблера не очень понимаю как и кем готовится поляна, и в первую очередь в части памяти. Поэтому предполагаю что живет она частично на предварительно подготовленном стеке, а частично делается той самой new() (допускаю, что плюсы меня сильно отравили, но все же). При этом я не вижу привычных операций по подготовке стека, очистки и инициализации сегментов данных и прочего. Того круче - я вообще не вижу обозначенных границ доступной памяти во всем проекте.

Т.е. какой-то очень специфический embedded/demo получается. И становится интересно - это недоработки проекта, полагания на то, что BOOT сектор на любой машине окажется по фиксированным адресам с фиксированнымы настройками сегментов памяти или это я слепой и не вижу где и как это все делается. В таких случаях мой арбитр всегда один - итоговый код. Ему работать - а значит в нем все будет.

А вообще интересно. Есть ли у Rust опция, которая позволяет оставлять промежуточные файлы (как у gcc)? Тот вполне в состоянии предоставить тот самый подстрочник. В реальном embedded весьма востребованный инструмент. Не ежедневного использования, но все же.

P.S.

О недоверии ассемблерным вставкам говорю не я. Я этому скорее удивляюсь. И пытаюсь понять почему так.

Поляна готовиться отдельным пакетом bootloader, а собирается это всё в образ с помощью bootimage.

И еще - а что реально такая большая разница между asm! в Rust и asm{} в С? Я полагал что это куда как более близкие родственники, чем #define.

макросы в Rust ближе к метапрограммированию в C++. Там происходит прямое обращение с элементами AST языка aka Token Tree.

Потом - не всякий ассемблер небезопасен.

Любой ассемблер небезопасен с точки зрения компилятора, потому что нельзя его представить как объект теории типов, для которого впоследствии можно математически вывести границы в памяти, время жизни и владение. Borrowck конечно силён, но он все ещё "игрушечный" солвер и проворачивать полноценную валидацию asm не может. Если сравнивать с каким-нибудь comp cert, конечно.

Чтение... На вскидку даже не предположу что и как оно может поломать.

В го чтение из закрытого канала вешает поток, например. Что мешает исполнить похожее поведение в железке? В общем-то ничего. Можно делать чтение не в том порядке, ломая какую-нибудь другую железную/софтварную логику или читать только куски длинных регистров, случайно отбрасывая значимые детали команд. Для известных железок-то оно конечно уже тыщу раз проверено и работает, но как ты докажешь компилятору, что это будет именно вот эта конкретная железка или процессор и что вот этот набор инструкций точно ничего не ломает. А у rust/clang поддерживаемых архитектур ох как немало, в рамках которых существует ещё больший зоопарк разных реальных железок с разной конфигурацией портов, памяти и иногда багов.

Ну да. С ростом кодовой базы, начинаем топтать те же грабли, что и C. Особенно опускаясь к тому же уровню исполнения. И с теми же проблемами. Этот арканоид никогда не будет работать, на чем-то отличном от PC c VGA. Но "зоопарк архитектур" уже начинает влиять.

Вообще мы с вами на разных языках разговариваем. Начиная с того, что память, разделяемая на память-память с произвольным доступом и порты ввода-вывода с доступом только с использованием пары специальных ассемблерных инструкций - это чуть ли не уникальный костыль PC. Понятно откуда взявшийся, но от этого не менее уникальный. От костыля этого, по сути, уже отказались и держат исключительно для совместимости с legacy решениями. Сравнивать его даже с регистрами периферийных блоков STM32 или AVR не самая корректная идея. Это физически очень разные сущности. Не говоря уже про программные каналы где бы то не было.

Да и ассемблер, в общем случае, оперирует очень небольшим количеством базовых типов (из которых тот же Rust плодит производные). И очень небольшим количеством операндов. Ваше строгое "нельзя" точно не соответствует истине. В конце-концов и при трансляции все сводится к ассемблерным инструкциям. Другое дело, что целесообразность этих операций вызывает серьезные вопросы. А вот вопрос как относиться к человеку, который пишет asm! - как к вредителю, не желающему пользоваться средствами языка, или как к профи, обходящему бутылочное горлышко... Впрочем, ваш ответ понятен. Имеете право.

Sign up to leave a comment.

Articles