Как стать автором
Обновить

Комментарии 53

"В правила работы с указателями и memcpy в С не заложены грамотные способы представления пустого среза памяти."

?! максимальное удивление. В "правила работы с указателями" вообще никакие "срезы" не заложены. Указатель + длина блока памяти. Передавайте нулевую длину. Нет адреса ? передавайте null указатель и нулевую длину. Хотите строго следовать стандартам и не верите в арифметику null? (const uint8_t*)0 вам в помощь! туда ноли можно прибавлять до одури!
memcpy отлично работает работает с нулевой длинной - её можно копировать откуда угодно и куда угодно в любый количествах.

50 лет C отлично работает с указателями и областями памяти, а тут выяснилось, что "не заложены грамотные способы работы"... Главное разработчикам системного софта об этом не говорить.

"В правила работы с указателями и memcpy в С не заложены грамотные способы представления пустого среза памяти."

это, к сожалению, всего лишь не грамотный перевод для:

  • C’s rules around pointers and memcpy leave no good ways to represent an empty slice of memory.

я не переводчик, и не знаток английского, исходя из общего понимания на техническом уровне я бы перевел так:

правила работы с указателями в С и memcpy не позволяют определить какое-то приемлемое представление для пустого среза памяти (возможно надо было переводить как "отрезок памяти нулевой длины", слово "срез" действительно режет профессиональный слух в данном контексте)

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

Тут, например, неплохо было бы пояснить что рассматривается проблема передачи таких "отрезков памяти нулевой длины" в качестве объекта и параметра при взаимодействии модулей написанных на разных языках с кодом написанным на Rust (С++ и Rust например).

Зачем профессиональные переводчики выкладывают фактически машинный перевод таких глубоко технических текстов было бы загадкой, если бы ...

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

напоминает что-то фольклерное:

кукушка хвалит петуха за то, что хвалит он кукушку

PS:

если я погибну, считайте меня комиссаром! Комиссаром Коррадо Каттани :(

Мне не нравится качество перевода, но саму статью я плюсанул, ибо торт. Особенно по сравнению с заполонившим хабр шлаком класса "гуглояндекс миня обидел, вот моя телега".

не знаю, сугубо мое личное мнение:

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

Признаю, статья тяжелая для перевода. И даже не столько по своей тематике, сколько по форме изложения содержания автором. Что касается срезов, то вот ссылка на пример употребления этого термина в профессиональном контексте. https://runebook.dev/ru/docs/rust/std/primitive.slice?page=2 И есть другие примеры в сети. Я это взял не из головы. В ходе изучения темы удалось найти только два ходовых варианта перевода "slice of memory" в этом контексте - срезы и, собственно, слайсы. Я предпочёл первый.

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

Спасибо вам за комментарий.

slice of memory можно перевести вовсе как "кусок памяти" на самом деле. Просто в контексте статьи там ещё размер прикреплён к нему, пусть и нулевой.

Байтовые и строковые срезы это термины, которые широко используются в Rust.

Срезы есть и в питоне, и в новых стандартах плюсов в виде views/spans.Так что не грибами едины

 передавайте null указатель и нулевую длину. 

 C standard (ISO/IEC 9899:1999) , memcpy: 

If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer, or a pointer to non-modifiable storage when the corresponding parameter is not const-qualified) or a type (after promotion) not expected by a function with variable number of arguments, the behavior is undefined.

Какой ужас... а ещё указатели разной размерности нельзя приводить. По факту в текущей кодовой базе данное поведение используется повсеместно, и почему в стандарте написано так, это вопросы к маразматикам, его пишущим. 90% кодовой базы C++ формально не соответствует стандарту. Но есть вещи, где без которых писать софт просто нельзя. Поэтому смело используем то, что гарантированно работает ( (void *)0x0 ), и объявляем это платформозамисимой частью, как это сделано абсолютно везде. Если найдётся платформа, где это не будет работать (нет, не найдётся), то для неё придётся приделать костыль. И да, взаимодействие rust<->C/C++ не может обойтись без платформозависимых компонентов, так как модели памяти, кэши, и прочее.
А как Java эту проблему решает? Python? .NET? У них у всех есть аналогичные проблемы. И они все решаются через признание некоторых частей платформо-/компиляторо- зависимыми, и оставляем их решение на конкретную реализацию Rust.
Стенания на ровном месте, вызванные тем, что автор не умеет в настоящее кросcплатформенное программирование.

Ок, понял, стандарт пишут маразматики, автор статьи не умеет в настоящее программирование, а вы великолепны. Спасибо за важную информацию.

Это такая манера на Хабре? Давать оценки авторам? Умеет не умеет. Вместо темы обсуждать автора это дешевая манипуляция, которая как бы возвышает критика над автором. Но это мало того что не прилично, но и дурно характеризует такого критика.

Это не манипуляция. Автор сокрушается про UB, но не задумывается, каким образом осуществляется взаимодействие с C/C++ в других языках и не владеет спецификой системного программирования на C/C++. Если бы автор внимательно посмотрел на все возможные случаи взаимодействия Rust и C/С++, то он бы ещё штук 30 UB нашёл бы. Ужас-ужас.
Да там много UB, но оно UB только без учёта платформы. Объявляем часть кода платформозависимым и UB пропадает. При портировании Rust на конкретную платформу, само собой, это всё нужно учитывать. malloc, работа с указателями и приведение типов являются UB только в стандарте C/C++ без учёта платформы. На каждой конкретной платформе там практически нет UB и все эти "хаки" активно используют. Более того, без них просто невозможно ничего сколько-либо низкоуровневого на C/C++ сделать. А взаимодействие C/C++ это низкоуровневая платформозависимая область.
И проблема здесь именно в том как формулируется и пишется стандарт C/C++.

Автор сокрушается про UB, но не задумывается, каким образом осуществляется взаимодействие с C/C++ в других языках и не владеет спецификой системного программирования на C/C++.

Автор работает в Google над BoringSSL (местным форком OpenSSL для использования у них же в Chrome/Chromium и т.п.). Т.е. он действительно занимается разработкой прикладного софта, а не компиляторов.

При портировании Rust на конкретную платформу, само собой, это всё нужно учитывать

У него задача не портировать Rust на конкретную платформу, а задизайнить API библиотеки так, чтобы при взаимодействии с кодом на Rust не терять в эффективности, и не вызывать когнитивный диссонанс.

Тогда нужно править стандарт C/C++ так как кроме malloc там ещё вагон и маленькая тележка аналогичных проблем. Но со стандартом там уже больше десяти лет цирк с конями, а потому остаётся только решать проблемы теми же способами, что и диды решале.
И то, что автор работает в Google над серьёзным проектом не означает, что он специалист по портированию софта на C/C++. Он может быть богом SSL и Rust, но при этом не иметь опыта системного программирования на C/C++.
Как я уже несколько раз замечал - совершенно аналогичная проблема есть в Pyhton/Java/C# и как-то они с уже 15 лет с этим всем живут, причём, вполне успешно и кроссплатформенно. И это только одна из десятков проблем, возникающий при попытке сделать портабельное взаимодействия какого-либо языка программирования с C/C++.

Тогда нужно править стандарт C/C++ так как кроме malloc там ещё вагон и маленькая тележка аналогичных проблем.

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

Что в итоге решил делать видно по комитам https://boringssl.googlesource.com/boringssl/+/refs/heads/master.

Ну вы можете посмотреть на опыт автора в программировании на С/C++, BoringSSL общедоступен: https://github.com/google/boringssl/commits?author=davidben

Внутреннего кода у него ещё больше, но тут NDA, так что поверьте на слово: достаточно. И обсуждения "вот это казалось бы общепринятое поведение разносит код вдребезги в таком-то сочетании компилятора/стандартной библиотеки/платформы" в их команде происходят с печальной регулярностью.

У джавы нет ни одного УБ, можно почитать стандарт при желании. У шарпа вроде парочка есть, но в супер эдж кейсах, я щас даже не вспомню что там конкретно нужно сделать.

Я про взаимодействие с C/C++. Т.е. про тот же JNI если мы про Java. Если заглянуть в стандартные хедеры конкретной JVM, то там сплошное UB и платформозависимые костыли.

Терминология тоже удивляет. Срез? А область памяти чем не нравится?

Неконцептуально.

Ну ладно С++, но к С-то чего докопались? Там совершенно иная философия. memcpy - это не про срезы, объекты или еще что. Это про блок памяти, который начинается с заданного указателя и имеет заданный размер. Все. Ничего более. Что лежит в этой области памяти уже должен думать разработчик. Помещается туда два ваших "объекта" или только полтора - это уже ваша головная боль и ваша ответственность.

Тем более под срезом обычно понимают slice\range

slice это термин из Rust. Он определяет "окно", через которое можно смотреть на массив данных за ним. Фактически это указатель, у которого определенны начало и конец (или длина).

В некоторых случаях полезно иметь возможность работать со слайсами нулевой длины (можно рассматривать например как аналог пустой строки в выражении "abc" + "").

Автор размышляет на тем, каким образом можно эффективно представить такое вот пустое значение в C и C++, чтобы при выполнении типичных операций обойтись лишних проверок. Например конкатенация с помощью двух пустых слайсов это валидное действие (как для строк "" + ""), но в случае наивной реализации - как пары значений (NULL, 0) - может привести к memcpy(NULL, NULL, 0), что в стандарте внезапно определено как UB.

автор странные вещи вещает. Это уже давно и успешно решается везде от ядра линукса до embedded софта.
(void*)(0x0)

(void*)(0x0)

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

#define NULL ((void *)(0))

#define NULL ((char *)0)

#define NULL 0L

#define NULL 0

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

Здесь нет проблем если так писать кастомный код на С, когда интероперабельность с Rust это ваша цель.

Но как решение уровня API оно не выглядит подходящим, поскольку подробный стиль не является нормальным ни для C, ни для C++.

Загляните в хедеры компилятора. Там чистый UB. Стиль совершенно нормальный так как мы говорим о низкоуровневом коде, который пишется с учётом конкретной платформы и конкретного компилятора.

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

А разработчик переносимой программы или библиотеки так не может делать тем более.

У разработчика нет другого выхода - посмотрите на исходники любого сложного портабельного софта. В течение 10 лет платфомозависимую часть придётся неоднократно дорабатывать под новые реалии.

Но это не повод делать платформозависимыми те вещи,, которые можно оставить независимыми.

"будет охватывать все объекты от start до end включительно " А разве end включается?

Вот да, хотел написать этот коммент. Он тем более удивителен, что дальше автор сам же пишет про массивы (срезы) нулевой длины сразу за последним элементом массива. Да и в формулах count = end - start он явно потерял единицу...

Я всегда в коде использую понятие limit вместо end. Верный способ застраховать себя от ошибки на единицу.

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

  • cначала можно было работать с нулевым адресом. Нормальный адрес - хочешь пиши, хочешь читай. В intel 8086 там была таблица векторов прерываний и по адресу 0x0000:0x0000 был адрес обработчика деления на 0 (хе-хе-хе, как много нулей);

  • потом ребятки из C решили, что адрес 0 - он такой особенный, поэтому давайте значение зарезервируем - и появился NULL;

  • потом программисты начали огребать проблемы с адресами рядом с 0. Например, поле структуры с NULL адресом может быть, например, в 0x0010. Чтобы разобраться с этой напастью для обычных программ (user mode) стали выделять блок памяти в (например, Windows) 64 кБайт с обеих сторон от NULL и считать эти адреса невалидными и явно крэшить программу при их использовании;

  • И ВОТ пришла лягушка rust и говорит: давайте эти небольшие адреса использовать как не NULL/nullptr: они как бы есть; но ими пользоваться напрямую нельзя; но вообще их использовать можно с пользой!!! W-T-F???

Сколько ещё издевательств готовит нам чудесное будущее??? Может хватит издеваться над языками???

--

Прим.: по поводу использования дополнительных проверок на NULL в Си: это всё уменьшение производительности (в ранних графических движках старались пиксели сразу перерисовывать или делать логические операции and/or/xor - главное уйти от лишних сравнений, которые для большого количества пикселей сразу приводили к просадкам производительности). Поэтому чистый Си даёт возможность сделать всё быстро. И НЕ НУЖНО ЭТУ ВОЗМОЖНОСТЬ ОТНИМАТЬ.

C подходом rust всё даже веселее, так как программа закаршится даже без явного обращения по null. Предвыборка данных процессора и всё такое.

Предвыборка данных процессора на это рассчитана. Собственно meltdown это и эксплуатировал

На какой платформе? На некоторых платформах предыборка невалидного адреса сразу валит в halt/exception/NMI.

На "дефолтной", x86. Мы про какую предвыборку говорим - которая от спекулятивного выполнения?

Предвыборка кэша. Она отрабатывает полноценные обращения к шине со всеми сопутствующими эффектами включая исключения. Предвыборка спекулятивного исполнения работает уже позже - с данными из кэш памяти. При этом кэш память грузится строками, что делает ситуацию веселее - обращаемся к одному адресу, а исключение может произойти в результате обращения предвыборки на 16 байт дальше. И на ARM всё ещё в пять раз веселее. Там вообще всё на совести программиста - сработало спекулятивное исполнение и выборка по запрещённому адресу - получи мгновенный hard fault или bus fault.

Если оптимизация изменяет поведение программы любым способом (включая краш) - это баг.

А ничего, что С появился на 9 лет раньше, чем Intel 8086, и нулевые указатели стали сразу же применять для обозначения отсутствия указываемого, поэтому в вашей истории первые два пункта перевёрнуты с ног на голову? NULL никто не резервировал, это макрос из stddef.h. Зарезервированным нулевой указатель стал только в С++11 в виде nullptr. Опять же выделять блок памяти 64кб для того, чтобы доступ к адресам около нуля приводил к ошибкам, на x86+ процессорах, где и появилась Windows, никак не требуется. Достаточно правильным образом написать обработчик процессорного исключения при доступе к отсутствующей в адресном пространстве пользовательского процесса странице.

А ничего, что ...

Да в общем-то, ничего. Intel 8086 приведён для примера, чтобы показать явное использование. Одна надежда, что нулевой адрес памяти придумали ДО языка Си.

С резервированием макроса всё намного интереснее. Формально, NULL - это дефайн и как-бы можно сделать компилятор, где это не нулевое значение. Однако в коде на Си очень часто используется проверка на валидный указатель вида:

void* p = ... ;

if (p) { ...

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

Про блок памяти моя неточность: выделять не в смысле аллоцировать, а выделять в смысле акцентировать внимание. Конечно же, блок памяти никто не резервирует. Скорее определяет доступную память/сегменты/др. таким образом, чтобы память около NULL числилась вне доступного диапазона.

Однако в коде на Си очень часто используется проверка на валидный указатель вида: ... И эта проверка проверяет именно на не-ноль.

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

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

Поэтому определение #define NULL ((void*)0) появилось в стандарте С довольно поздно

Зарезервированным нулевой указатель стал только в С++11 в виде nullptr.

Так вроде nullptr не обязан быть нулевым указателем. Более того, на некоторых архитектурах он совсем не нулевой. И вроде вводили его специально, чтобы отучить программистов сравнивать указатель с нулем или NULL.

"if ( !nullptr )" всегда true. В силу этого nullptr не может быть не нулевым значением. Ну или нужо городить много очен костылей в комипляторе.

У nullptr свой тип nullptr_t! В чём сложность для одного отдельно взятого типа сделать "специализацию" под платформу?

Уж где-где, а в C++ bool operator!(const T &a); может выдавать хоть фазу Луны.

Это по стандарту так или в конкретной реализации?

Так-то в x64/x86 завсегда можно привести nullptr, как и любой другой адрес, к intptr_t, и оно будет ноль, но надеяться на то, что оно в любой архитектуре так будет, вроде как не приходится, и есть сие путь к UB.

Как раз нулевым указателем он быть обязан. Но не обязан указывать на нулевой адрес.

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

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

Теперь в обратную сторону, что если нулевой слайс пришел из с++, да очень просто используйте ас_реф который возвращает опшионал, а на нем анврап_ор с дефолтным/пустым слайсом. Да чуть больше накладных расходов, но не ужас ужас.

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

Кажется, вся проблема от того, что создатели Rust решили сэкономить на спичках дабы Option<&[T]> поменьше весил, но огребли проблем там, где не ждали. А по факту экономия так себе, ибо Option<&[T]>не сильно часто используется, ибо семантическая разница между пустым срезом и отсутствием среза мало где имеет значение, а значит лучше использовать просто &[T].

Что именно они огребли? За почти 10 лет ни разу не огреб ни одной проблемы из-за такого представления.

Стесняюсь спросить а почему "легко забыть"? Эдак любые правила можно считать что "легко забыть".

Зарегистрируйтесь на Хабре, чтобы оставить комментарий