Обновить

Ассемблер: рассматриваем каждый байт «Hello, World!». Как на самом деле работают программы на уровне процессора и ОС

Уровень сложностиСредний
Время на прочтение25 мин
Охват и читатели30K
Всего голосов 96: ↑95 и ↓1+113
Комментарии50

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

Чтобы разобраться, "как на самом деле работают программы на уровне процессора и ОС", потребуется не одна статейка на Хабре, а пара толстенных талмудов, как минимум. Ну а статья -- лишь беглый и очень поверхностный обзор всего этого, если исходить из заявленного в заголовке.

И, кстати, неверно сказать, что виртуальная память создаёт для программы впечатление, что ей (программе) доступна вся имеющаяся на машине память. Виртуальная память просто полностью "отвязывает" программу от реально имеющейся памяти. Но программе, во-первых, всё равно доступны не все адреса -- скажем, Винда забирает под себя старшую половину адресного пространства (наследие VAX/VMS, где это было следствием архитектуры машины); в Линухе и других системах ситуация принципиально такая же, различаются лишь детали. А во-вторых, программе благодаря виртуальной может быть доступен больший объём памяти, чем физически имеется на машине, причём иногда, особенно во времена уже достаточно отдалённые, во много раз больше.

Да, согласен, процессу может быть доступно и больше памяти(RSS/VSZ). Как JVM, которая аллоциирцет память гигабайтами. Но физически же больше памяти не всунешь, я об этом

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

Спасибо за критику и комментарий!

Ну в своп всунешь, не проблема.

Комментатор выше писал, что программе может быть доступен больший объём памяти, чем физически имеется на машине, это немного не про своп. Как я и написал, JVM вообще терабайтами аллойирует память, что уже больше, чем HDD/SSD + RAM.

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

Топик начался с того, что этого в статье я не упомянул, ограничившись swap разделом, дабы совсем не запутать новичков. Но ошибок в объяснении, как по мне, я не сделал в статье, просто опустил некоторые детали

Движок Chrome тоже делает себе маппинги виртуального адресного пространства на терабайты и реально маппит туда куда более скромное количество страничек. Делается это из-за реализации современных песочниц для JIT'а. JIT генерит код и разбрасывает его рандомно по адресному пространству процесса в маленькие замаппленные странички, а большая часть этого гигантского пространства остаётся пустой. Так что написать рабочий эксплоит становится очень сложно - шанс угадать адрес скомпиленной функции в памяти, чтобы сделать jump туда крайне низок. При этом если даже каким-то образом удастся заставить код, созданный JIT'ом прыгнуть за пределы странички памяти, в которой он лежит - скорее всего, он наступит на незамаппленную страничку, и ОС просто пристрелит процесс из-за Page Fault (SIGSEGV или в винде - Access Violation).

Интересно, не знал таких подробностей про JIT 🤔

Про половину. Видимо поэтому безопасно использовать signed integer вместо unsigned для размеров? Допустим, если используем 64-битные адреса, то ОС принципиально не выделит памяти больше чем 2^63 - 1, и тогда удобно использовать int64_t (или ptrdiff в более общем случае) для реверсивного обхода контейнеров:

for (int64_t i = vector.size() - 1; i >= 0; i--) {
  do_work(vector.at(i));  
}

..Ассемблер это уже пережиток. Его применение только в системах без ОС и с памятью 1 килобайт rom и 256 байт ram. Как написавший не одну тысячу строк на ассемблере утверждаю, что все остальное можно быстро, корректно, качественно, поддерживаемо и повторно использемо можно сделать минимум на С.

Разумеется, но понимать же стоит как всё работает на его уровне, это база, как по мне

Такие вещи помогают лучше понять происходящее на экране ;)

"всё остальное" на сях не сделаешь: всегда остаются вещи, которые можно сделать только на ассемблере -- в частности, переключение контекста.

Как только на ассемблере? А long jumps на что?

Лонг джампс это и есть ассемблер. Но тут речь за работу с регистрами таблицы страниц, превилегированные инструкции, если речь за х86/х64.

setjump() и longjump() это не ассемблер, а библиотечные функции стандарта ANSI C.

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

Что-то типо этого: https://git.musl-libc.org/cgit/musl/tree/src/setjmp/x86_64/setjmp.s

Вопрос был, надо ли программисту осваивать ассемблер, например, для переключения контекста.
Ответ: если для платформы полностью поддерживается стандартная библиотека, то нет.
На MS DOS она не поддерживалась, поэтому там переключение контекста я делал сам (когда в институте учился). На ассемблере.
В основном осваивать ассемблер осваивать надо тем, кто пришет компиляторы и стандартные библиотеки.

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

А компиляторы и стандартные библиотеки что, не программисты пишут?

Интересно, что вы скажете о, например, __SEI() для ARM GCC и прочем подобном.

Нестандартные расширения языка -- те самые intrinsic. Появляются как раз из-за того, что на чистых сях многие очень уж низкоуровневые вещи сделать нельзя.

... который внутри себя использует ассемблер. Шах и мат.

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

А как же платформозависимые части библиотек(кода)? Например, переключение контекста, о котором упоминал человек выше

На самом деле, могут быть даже формально независимые вещи, которые, однако, есть смысл писать на ассемблере ради производительности. Скажем, всякие перекодировки между UTF-8/16/32, пересылки строк, шифрование DES/AES и ряд других операций современные IBMовские мэйнфреймы умеют делать одной командой; соответственно, реализующие их подпрограммы для эффективности должны опираться не на "обычную" систему команд, используемую компилятором, а прямо быть написаны на ассемблере (ну или оформлены внутри компилятора как intrinsic, что, в первом приближении, то же самое, только переносит ассемблерный код из библиотеки в компилятор).

Я не про это. Я про оптимизации разного рода - там вполне себе захардкожены ассемблерные вставки, которые компилятор подставляет в вывод. Это и есть "внутри себя". И это помимо использования платформозависимых оптимизаций, о которых упомянули выше.

Причём "исторически" -- в GGC и CLANG. Древние трансляторы транслировали сразу в машинный код -- процы и без того хилые были, чтобы выполнять две трансляции вместо одной.

Оригинальный компилятор Ричи выдавал ассемблер, как раз потому что адресное пространство на PDP-11 было ограничено 64/128KB. И даже это делалось в два прохода, каждый из которых был отдельной программой. В три, когда появился препроцессор.

Оригинальный компилятор Ричи не был первым компилятором в истории. В частности, на той же самой PDP-11 с теми же самыми ограничениями существовали DECовские компиляторы: Фортран, Паскаль, Кобол и Бейсик (был ещё Бейсик-интерпретатор с совсем другим диалектом). Так вот, все эти компиляторы выдавали сразу объектные файлы. Компилятор Паскаля мог, если ему указать соответствующий параметр, выдать и ассемблерный текст (насчёт остальных не знаю -- я использовал Паскаль и ассемблер). Точно так же обстоит дело и с другими архитектурами.

Речь ведь очевидно шла про C.

Но даже про фортран, первый дековский фортран для PDP-11 (FORTRAN II) генерировал шитый код. Про дековские компиляторы паскаля/бейсика/кобола я никогда не слышал.

Ну уж нет, на С эксплуатировать уязвимости типа buffer overflow, мягко говоря, неудобно (хоть и возможно).

add rdi, rax

А результат точно в rdi? Нет, есть конечно ассемблеры в которых dst первым идёт, но твт же x86 вроде

В интеловских процессорах, как и в большинстве других архитектур, приёмник -- первый операнд. Последний в роли приёмника -- у DECовских машин, что весьма и весьма неудобно (например, путаница в SUB и CMP: первая вычитает справа налево, а вторая -- слева направо). Просто те клоуны, которые сделали древний gas, для чего-то решили натянуть дурацкий DECовский синтаксис на другую (интеловскую) систему команд.

Да, почитал уже про этот зоопарк. В руководстве Oracle по ассемблеру x86 от 95 года тоже dst - второй. Система команд AT&T, типа.

Умеете объяснять. Куда лучше, чем во многих читанных мною книгах.

Годная статья для вкатунов. Как мне, возможно наивно, кажется, читать хотя бы настолько упрощённые изложения основ важно даже перекладывателям JSON'ов. Даже если ничего не запомнится напрямую, всё равно в головах будет чуть меньше магического мышления.

Что я всегда делаю с такими постами? Правильно! Добавляю их в закладки, чтобы когда-нибудь прочитать и стать крутым программистом

Уф, мне бы такой текст лет тридцать назад, когда я спрашивал, что делает компилятор в Turbo Pascal...

Замечательная статья.

Зачем эльфы и сисколлы? Можно ж прямо из бутсектора в процессор заходить xD: https://habr.com/ru/articles/490094/

А если отмести шутки и саморекламу — это круто, особенно среди тонны статей о том, как LLM скоро всех нас пустят на электричество для ЦОДов.

Картинка с Trigger ввела в ступор на некоторое время.

Не Flip-flop имелся ввиду, случайно?

о какой картинке вы говорите? была картинка с регистром. регистр состоит из базовых элементов памяти(цифровых автоматов) — триггеров. триггеры же состоят из транзисторов. регистр — массив триггеров. вы об этом?

Всё верно, если вы пишете "register", то он состоит из "flip-flop"-ов. А если вы пишете "регистр" то он состоит из "триггер"-ов. "Trigger" это спусковой крючок у оружия.

я это знаю как D-trigger или схему из нескольких D-trigger’ов ;) вы же о них? flip-flop где-то слишком глубоко в моей памяти был

Разница между буржуйской и отечественной терминологией. Буржуйская более чёткая: у них либо flip-flop, либо latch. Последние у нас именуют защёлками, но могут назвать и триггерами, первые -- только триггерами. Из-за этого регулярно возникает путаница.

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

Посыл предыдущего оратора был в другом. А что касается "защёлок" так это разновидность триггера: прозрачный триггер.

Посыл предыдущего оратора был в другом.

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

А что касается "защёлок" так это разновидность триггера: прозрачный триггер

Никогда не встречал, чтоб их на русском называли "прозрачными триггерами". Вот "триггерами типа "защёлка"" -- постоянно.

Никогда не встречал, чтоб их на русском называли "прозрачными триггерами". Вот "триггерами типа "защёлка"" -- постоянно.

Это всего лишь логический вывод. Есть "прозрачная защёлка" и "непрозрачная защёлка" (термин применялся даже в журнале Радио, но вы можете погуглить "transparent latch"). Так вот, непрозрачная это же типичный D-триггер. А в библиотеках CELLS для САПР такой триггер собирается из... паравозика "прозрачных защёлок"!

Как оно реализовано в кристалле 2016 года выпуска

Схема прозрачной защёлки:

Реализация в кристалле:

Схема D-триггера:

Реализация в кристалле:

да, наверное немного упростил в контексте той картинки

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

Рад, что вам так понравилось :)

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

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud