<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
  <channel>
    <language>ru</language>
    <title>Низкоуровневое программирование</title>
    <description>Статьи для всех, кто интересуется разработкой операционных систем и тонкостями байткода.</description>
    <link>https://tproger.ru/tag/low-level-coding</link>
    <atom:link href="https://tproger.ru/tag/low-level-coding/feed" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Sat, 24 Oct 2026 09:38:20 GMT</lastBuildDate>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>12</sy:updateFrequency>
    <image>
      <title>Низкоуровневое программирование</title>
      <link>https://tproger.ru</link>
      <url>https://tproger.ru/apple-touch-icon.png</url>
    </image>
    <item>
      <title>Как я писал ОС, но не под x86, а под RISC-V</title>
      <link>https://tproger.ru/articles/kak-ya-pisal-os-no-ne-pod-x86-a-pod-risc-v</link>
      <comments>https://tproger.ru/articles/kak-ya-pisal-os-no-ne-pod-x86-a-pod-risc-v?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[lmemq]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/kak-ya-pisal-os-no-ne-pod-x86-a-pod-risc-v</guid>
      <description><![CDATA[<p>Решил разобраться, как устроены операционные системы изнутри, и написал свою простую ОС под RISC-V. Делюсь написанием многопоточности, сохранением регистров процессора и другими интересными кейсами</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/kak-ya-pisal-os-no-ne-pod-x86-a-pod-risc-v">Как я писал ОС, но не под x86, а под RISC-V</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Open Source]]></category>
      <category><![CDATA[Операционные системы]]></category>
      <category><![CDATA[C++]]></category>
      <category><![CDATA[Язык ассемблера]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 23 Jul 2026 13:21:29 GMT</pubDate>
      <content:encoded><![CDATA[<h3>Введение и дисклеймер</h3><p>Сразу скажу - я не считаю себя втором Терри Дэвисом и тем более не хочу делать аналог Виндовс. Изначально я все это затеял только ради прикола и изучения ассемблера.</p><p>А, и если что - мне 14, но я не против любой критики. И да, я тоже как и большинство местами использовал ИИ. Я не сидел читал мануалы часами, просто гуглил или просил нейронку пояснить.</p><h3>Так а в чем же проблема x86_64?</h3><p>Я так скажу - я пробовал писать под эту архитектуру. Мне не понравилось, всякие режимы (которые за меня правда переключал загрузчик), да и местный ассемблер мне было лень изучать. Просто захотел писать под более новую архитектуру.</p><p>Не буду говорить, что RISC-V лучшая, но я просто выбрал ее. Прикол в том, что инструкции короткие, да и в целом достаточно перспективно. Хотя и тут были проблемы.</p><h3>Был драйвер, стала ОС</h3><p>Я ничего с нуля не писал.Все началось с пыток ИИ, когда я пытался выдавить из него то обращение по конкретному адресу для доступа к графике, то опрос портов, то еще что-то такое. Мне стало лень и я пошел искать что-то плюс-минус готовое. И я нашел его - <a href="https://github.com/CityAceE/qemu-ramfb-riscv64-driver">ramfb</a>. По сути я получил готовый указатель на фреймбуфер для вывода на экран… И все. Но и на этом большое спасибо авторам драйвера. Дальше началась моя ОС.</p><h3>А где хранить данные?</h3><p>А тут первая проблема - как таковой кучи у меня еще нету. Есть стек, который за меня выделил автор драйвера. Есть всякие глобальные переменные. Но что если мне нужно много памяти (например, под картинки) и я не знаю точный размер? И в добавок, кто вообще рисует напрямую на экран? Тут нужен некий массив пикселей, который будет лежать в памяти, а по нашей команде быстро перенесется на экран (называется backbuffer).</p><p>Под кучу взял 64 мб из памяти, так как Qemu, где запускается моя ОС, выделяет ей аж 128 мб, часть уже занята ядром и стеком, но бОльшая часть пустует. Сделал простенький список свободных и занятых блоков памяти.</p><p>Это когда ты делишь память на куски, каждый помечаешь как занятый или свободный. Нужно мало памяти - откусываем от большого куска блок. Блок освободился, а рядом есть другой свободный блок - соединяем в один большой.Сразу же добавил функцию, чтобы узнать сколько памяти занято. Сейчас, например, занято около 3-4 мб. Ну это учитывая, что backbuffer и занял почти 3.5 мб.</p><p>В оперативной памяти что-то типа</p><h3>Такая разная многопоточность</h3><p>Как я думаю большинство знает, если у процессора одно ядро, одновременно на нем может выполняться только одна задача-поток. Но мы же может останавливать задачу, запускать другую, останавливать, запускать еще другую и так много раз в секунду. Тогда будет казаться, будто бы у нас несколько задач выполняются одновременно.</p><h4>Многопоточность кооперативная</h4><p>Так мы можем сказать потокам, чтобы если один из них решит, что стоит сделать паузу, сам вызвал yield() и отдал процессор другому потоку. И я это сделал. Все что нужно было - сохранить s-регистры процессора (те, в которых лежат всякие важные значения), указатель на личный стек потока (размер стека задается при создании) и указатель на “дорогу назад” ra. То есть yield() сохраняет эту информацию потока А, затем вместо нее загружает информацию потока Б. И так по кругу.</p><p>Кстати, стеки самих потоков я выделял в той самой куче. Просто брал блок на пару кб и говорил потоку: “Это твой стек”.</p><p>Но у такого концепта есть проблема - зависает один поток, остальные перестают выполняться. Да и в целом писать yield() каждый раз сложно.</p><h3>Многопоточность вытесняющая</h3><p>Поэтому я и решил переписать все на вытеснение. Для начала я сделал так - каждые 100к тиков процессора сохраняются все регистры и пинается функция обработки прерываний. Таймер, по которому будет работать многопоточность - это тоже прерывание. Так же эта функция ловит ошибки по типу kernel panic. Это было сложнее - так как потоки не добровольно отдают процессор, приходится сохранять вообще все 32 регистра. Но криво-косо я написал это и функцию sleep(), во время которой функция простаивает, ее никто не трогает.</p><p>А тут еще и ИИ начал выдавать бред, пока я тестировал у меня… начинал выполняться основной код, все было хорошо, а потоки - нет. Я сам не вижу ошибку, ИИ тоже. Он то предложит заменить какую-то часть функции, то еще что-то такое. Наконец я вспомнил про замечательную штуку - gdb. Запустил, посмотрел… Оказалось так - цикл отрисовки экрана был слишком долгим, во время него вызывалось переключение потоков, а их еще не было кроме главного. В итоге я подумал что смысла создавать потоки в конце, после отрисовки, нет, и перенес их в самое начало. Удивительно, но все заработало.</p><h3>Немного про режимы</h3><p>Как выяснилось, в RISC-V есть 3 режима (точнее 4, включая гипервизор, но тут он мне точно не нужен) - M-mode с полными привелегиями, S-mode для ядра ОС (я думал что и мое его использует), ну и U-mode для пользователей, но до этого мои ручки еще не дошли.</p><p>Ну и по пути я случайно осознал, что мое ядро выполняется аж в М-режиме:</p><p>Вызываю я ecall из потока. Смотрю - kernel panic. В логах какой-то код “11”. Оказалось, это и есть ecall - в M-mode он 11, а в S-mode (в котором, как я думал, работало ядро, и который я пытался выловить) это 9. Ну когда я нашел, уже быстро исправил.</p><h3>Визуальчик</h3><p>Но в это время на сам экран эмулятора Qemu… Ничего и не выводилось. Совсем. Просто белый экран. Хотелось красоты.</p><p>Тогда я вспомнил, что день назад прикрутил qoi.h к моему коду. QOI - формат изображений. Выбрал я его, так как декодер легкий и его удобно использовать в любой ОС, где есть базовые функции.</p><p>Я аккуратно, разумеется, согласно лицензии, взял из pop os, которой когда-то пользовался, картинку для фона. Конвертировал ее в .qoi, а так как моя ос еще не умела читать с диска, перевел картинку в заголовочный файл .h. Весит правда много, но влезает.</p><p>Дальше я нашел шрифт (Liberation Mono от Red Hat), перегнал и его в .h. Честно, визуал меня не сильно интересовал, поэтому быстро попросил ИИ переделать функции вывода в консоль в функции вывода на экран. Проверил, просмотрел что и как сделано, получилось нормально для старта. Правда пока что в графической консоли нет нормальной перемотки и много чего другого.</p><h3>Вывод</h3><p>Конечно, еще нету даже ввода с клавиатуры, мыши, файловой системы (даже простой). Но я считаю, что вышло неплохо, пусть и далеко от уровня реальной ОС типа Линукса. Понятно, что код не очень-то красивый, достаточно простой. Статью я выкладываю больше просто чтобы разобраться самому в том, что я написал.</p><p>Если я не забью на код, хотел бы написать еще и MMU и переход в U-mode.</p><p>Для тех кому очень интересно - можете глянуть исходный код <a href="https://github.com/lmemq/mqos-rv">mqos</a>, там же будут инструкции по запуску.</p>]]></content:encoded>
    </item>
    <item>
      <title>KolibriOS 0.7.7: ОС на ассемблере, которая влезает на дискету</title>
      <link>https://tproger.ru/articles/kolibrios-0-7-7-os-na-assemblere-kotoraya-vlezaet-na-disketu</link>
      <comments>https://tproger.ru/articles/kolibrios-0-7-7-os-na-assemblere-kotoraya-vlezaet-na-disketu?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Евгений Стребков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/kolibrios-0-7-7-os-na-assemblere-kotoraya-vlezaet-na-disketu</guid>
      <description><![CDATA[<p>KolibriOS 0.7.7 — российская ОС на ассемблере, которая помещается на дискету и работает на i586 с 8 МБ ОЗУ. Разбираем возможности и ограничения.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/kolibrios-0-7-7-os-na-assemblere-kotoraya-vlezaet-na-disketu">KolibriOS 0.7.7: ОС на ассемблере, которая влезает на дискету</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Ретро]]></category>
      <category><![CDATA[Open Source]]></category>
      <category><![CDATA[Операционные системы]]></category>
      <category><![CDATA[Оптимизация]]></category>
      <category><![CDATA[Инструменты]]></category>
      <category><![CDATA[Hardware]]></category>
      <category><![CDATA[Производительность]]></category>
      <category><![CDATA[Браузеры]]></category>
      <category><![CDATA[Язык ассемблера]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Sat, 04 Jul 2026 12:28:13 GMT</pubDate>
      <content:encoded><![CDATA[<p>В 2026 году операционная система, которая полностью умещается на 3,5-дюймовой дискете и загружается быстрее, чем вы успеваете отпить кофе, звучит как технологическая шутка. Но <b>KolibriOS 0.7.7</b> существует, развивается и даже имеет рабочий графический стол. Это не эмулятор и не демо: полноценная 32-битная ОС, ядро которой написано на ассемблере.</p><p>В своей колонке <i>Daily Drivers</i> редактор Hackaday Дженни Лист скачала ISO-образ, запустила KolibriOS на современном ThinkPad и удивилась: система выглядит зрелой, стабильной и по-настоящему быстрой. Разбираем, что это за проект, откуда он взялся и кому пригодится сегодня.</p><h2>Что такое KolibriOS</h2><p><b>KolibriOS</b> — российский форк MenuetOS, созданный в 2004 году. В то время как автор оригинального MenuetOS Вилле Турьянмаа переключился на закрытую 64-битную версию, сообщество вокруг русскоязычного порта сохранило код открытым и продолжило развивать 32-битную ветку. Название «Колибри» выбрано неслучайно: система маленькая и быстрая, как птица-колибри.</p><p>Проект поддерживается преимущественно разработчиками из России, Казахстана, Украины и Германии. Код распространяется под открытой лицензией, а сборки регулярно публикуются на официальном сайте и зеркалах сообщества.</p><h2>Версия 0.7.7</h2><p>Версия 0.7.7 продолжает философию предельного минимализма. Минимальные системные требования смехотворны по нынешним меркам: <b>1 МБ</b> места на диске, <b>8 МБ</b> оперативной памяти и процессор класса <b>i586</b>. При этом система выдаёт графический интерфейс, многозадачность и набор встроенных программ.</p><p>Дженни Лист отмечает, что на ThinkPad 2020-х годов KolibriOS загружается «в мгновение ока» и сразу показывает рабочий стол. Интерфейс выглядит пиксельным, как в 1990-х: нет сглаживания, есть ощущение ретро. Но при этом всё работает — без синих экранов и зависаний.</p><ul><li>KolibriOS 0.7.7 — 32-битная ОС с ядром на ассемблере, базовый образ весит около 1 МБ.</li><li>Минимальные требования: i586, 8 МБ ОЗУ, 1 МБ на диске.</li><li>В комплекте браузеры, игры, эмуляторы, графические редакторы и среда разработки.</li><li>Главное ограничение для повседневного использования — отсутствие поддержки HTTPS.</li><li>Лучше всего подходит для старого железа, тонких клиентов, обучения и ретрокомпьютинга.</li></ul><h2>Как устроена система</h2><h3>Ядро на ассемблере</h3><p>Большая часть KolibriOS написана на <b>FASM</b> — плоском ассемблере. Это объясняет крошечный размер исполняемых файлов и молниеносную загрузку: система не тратит время на инициализацию абстракций, слоёв совместимости и десятков фоновых служб.</p><p>Важная оговорка: несмотря на миф «всё на ассемблере», часть приложений и драйверов портирована или написана на C, Free Pascal, Oberon и даже Python. Но ядро, графическая подсистема и системные вызовы остаются ассемблерными.</p><h3>Файловые системы и загрузка</h3><p>KolibriOS умеет загружаться с CD/DVD, USB, жёстких дисков и даже с классической дискеты 1,44 МБ. Поддерживаются файловые системы FAT12/16/32, exFAT, NTFS и ext2/3/4. Есть варианты для Coreboot и загрузка прямо из Windows.</p><h2>Что внутри: программы и игры</h2><p>Несмотря на размер, KolibriOS поставляется с обширным набором софта. По данным сообщества, в образ входят более <b>250 программ</b>, включая текстовый процессор, просмотрщик изображений, графический редактор, музыкальный плеер, IRC-клиент и веб-браузеры.</p><p>Из игр и развлечений — порты эмуляторов, DOSBox, а также shareware-версии классики вроде DOOM и Wolfenstein 3D на полноценном CD-образе. Есть поддержка SDL, что позволяет переносить проекты с «больших» систем.</p><blockquote>Something this polished deserves a while to play around.</blockquote><h2>Главное ограничение: интернет без HTTPS</h2><p>Дженни Лист называет именно этот момент решающим: встроенные браузеры KolibriOS, включая Netsurf и Webview, не поддерживают HTTPS. Без этого современный веб практически закрыт: большинство сайтов, включая сам Hackaday, просто не откроются.</p><p>Это меняет статус системы с «основной ОС на каждый день» на «платформу для специальных задач». Почта, мессенджеры, облачные сервисы и онлайн-документация остаются недоступны без промежуточного прокси или другой машины.</p><p><b>Почему HTTPS сложно:</b><br />Современный TLS требует большой криптографической библиотеки, регулярных обновлений корневых сертификатов и поддержки множества алгоритмов. В условиях ОС размером в мегабайт это серьёзный архитектурный вызов, который сообщество пока не взяло в штатный комплект.</p><h2>Кому и где пригодится KolibriOS</h2><p>С учётом ограничений у KolibriOS остаётся несколько чётких сценариев применения. В России и соседних странах это особенно актуально: парки старого офисного и школьного оборудования часто содержат машины, которые официально «не тянут» современные ОС.</p><ul><li>Воскрешение старого железа. Pentium- и ранние Core-машины получают рабочий стол, браузер для локальных страниц и офисные утилиты.</li><li>Тонкие клиенты. Низкие требования к RAM и CPU делают KolibriOS кандидатом для терминалов с удалённым рабочим столом.</li><li>Обучение. Ядро на ассемблере, простые системные вызовы и небольшой кодовой базы — отличная площадка для изучения устройства ОС.</li><li>Ретрокомпьютинг. Запуск DOS-игр через эмулятор, классические 2D-игры и ностальгический интерфейс 1990-х.</li><li>Встроенные системы. Сборка Kolibri-A ориентирована на специализированное железо и железные проекты.</li></ul><h2>Как попробовать KolibriOS</h2><p>Попробовать систему проще всего в виртуальной машине. Скачайте ISO-образ с официального сайта, создайте VM с 64 МБ ОЗУ и загрузитесь с образа. Для реального железа подойдёт запись образа на USB-флешку или CD.</p><ul><li>Скачайте ISO или образ дискеты с официального сайта KolibriOS.</li><li>Для VM: создайте 32-битную машину с 64 МБ RAM и VESA-видео.</li><li>Для железа: запишите ISO на CD или USB через обычную утилиту.</li><li>Загрузитесь и выберите разрешение экрана из меню загрузчика.</li><li>Исследуйте меню «Пуск»: игры, утилиты, терминал и браузер.</li></ul><p>Если планируете использовать сетевые функции, проверьте совместимость сетевой карты по <a href="https://wiki.kolibrios.org/" rel="noopener noreferrer">списку оборудования</a>. Поддерживаются многие чипы Realtek, Intel и 3Com.</p><h2>Выводы</h2><p>KolibriOS 0.7.7 — это не замена Windows или Linux, а демонстрация того, какой отклик может давать компьютер, когда софт пишут под железо, а не под абстракции. В эпоху, когда операционки растут гигабайтами, проект напоминает: минимализм ещё жив.</p><p>Для российского читателя у KolibriOS есть дополнительный смысл: это открытый проект с русскоязычным сообществом, который можно изучать, дополнять и применять для решения практических задач — от перевода старого классного компьютера в рабочее состояние до обучения низкоуровневому программированию.</p><p>Главный вопрос — нужна ли вам такая система прямо сейчас. Если у вас есть старый 32-битный ноутбук, интерес к ассемблеру или просто ностальгия по быстрым ОС — попробуйте. А для повседневной работы с интернетом пока придётся держать под рукой что-то посовременнее.</p><p><b>Источники:</b></p><ul><li><a href="https://hackaday.com/2026/07/02/jennys-daily-drivers-kolibrios-0-7-7/" rel="noopener noreferrer">Jenny’s Daily Drivers: KolibriOS 0.7.7 — Hackaday</a></li><li><a href="https://distrowatch.com/kolibri" rel="noopener noreferrer">KolibriOS на DistroWatch</a></li><li><a href="https://wiki.kolibrios.org/" rel="noopener noreferrer">Официальная вики KolibriOS</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>Async Rust: как уменьшить бинарник, пока rustc не оптимизирован</title>
      <link>https://tproger.ru/articles/async-rust-kak-umenwit-binarnik-poka-rustc-ne-optimizirovan</link>
      <comments>https://tproger.ru/articles/async-rust-kak-umenwit-binarnik-poka-rustc-ne-optimizirovan?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Евгений Стребков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/async-rust-kak-umenwit-binarnik-poka-rustc-ne-optimizirovan</guid>
      <description><![CDATA[<p>Разбираем, почему async Rust генерирует лишние state machine, как это бьёт по embedded и WASM, и что можно сделать в коде прямо сейчас.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/async-rust-kak-umenwit-binarnik-poka-rustc-ne-optimizirovan">Async Rust: как уменьшить бинарник, пока rustc не оптимизирован</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Оптимизация]]></category>
      <category><![CDATA[Многопоточность]]></category>
      <category><![CDATA[Компиляторы]]></category>
      <category><![CDATA[Производительность]]></category>
      <category><![CDATA[Rust]]></category>
      <category><![CDATA[Асинхронное программирование]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Mon, 29 Jun 2026 10:21:42 GMT</pubDate>
      <content:encoded><![CDATA[<p>Асинхронный Rust обещал нам zero-cost abstraction: пишете почти как на обычном синхронном языке, а runtime сам разбирается с конкурентностью. На сервере с десятками гигабайт RAM это обещание обычно сбывается. Но стоит перейти к embedded-прошивке на 256 КБ flash или к WebAssembly-модулю, который гонится за каждым килобайтом, — и картина меняется. Функция с двумя await-точками превращается в state machine из 360 строк MIR, тогда как синхронный аналог укладывается в 23. Почему так происходит и что с этим делать уже сегодня?</p><p>Тема получила новый импульс после серии публикаций Диона Доктера (Dion Dokter) из голландской компании Tweede golf. Во второй части он заглядывает внутрь rustc и утверждает: async Rust так и не вышел из состояния MVP. Я не буду дословно переводить его рассуждения, а переосмыслю их с упором на практику: как понять, платите ли вы за async-удобство лишними байтами, и как снизить налог, пока компилятор не научился делать это сам.</p><ul><li>Асинхронный Rust компилируется в state machine: на каждую await-точку приходится отдельное состояние плюс служебные Unresumed, Returned и Panicked.</li><li>Простая функция с двумя await-точками даёт 360 строк MIR против 23 у синхронного кода. Часть излишков LLVM убирает, но не все, особенно при оптимизации по размеру.</li><li>Компилятор не схлопывает идентичные состояния, не инлайнит futures через await, генерирует state machine даже для async-блоков без await и оставляет panic-путь в состоянии Returned.</li><li>В release-сборках замена panic в Returned на возврат Pending может сократить прошивку на 2–5%. Убирание state machine у пустых async-блоков — ещё около 0,2%.</li><li>Пока rustc не исправлен, можно вручную: возвращать impl Future из «прозрачных» функций, использовать std::future::ready, схлопывать await-точки через match и передавать большие данные по ссылке.</li></ul><h2>Почему async-функция — это всегда state machine</h2><p>Когда вы пишете async fn, компилятор не просто оборачивает тело в callback. Он строит конечный автомат: enum, где каждый вариант — состояние между await-точками. Это называется coroutine desugaring и происходит на уровне MIR (Mid-level IR) — до того, как код попадает в LLVM.</p><p>Рассмотрим минимальный пример. Есть две функции, каждая из которых просто возвращает число, и третья складывает результаты:</p><p>У bar две await-точки, поэтому минимум два пользовательских состояния. Но если сдампить MIR после прохода coroutine_resume, картина шире:</p><p>Три служебных состояния добавляются всегда. Они нужны, чтобы соблюсти контракт Future::poll: опрос завершённой future не должен приводить к неопределённому поведению. Поэтому после первого Ready future переходит в Returned, а повторный poll паникует. Аналогично Panicked блокирует future после пойманного panic — похоже на poisoning мьютекса.</p><p>Логика корректная, но цена высокая: bar порождает 360 строк MIR, а эквивалентный синхронный код — 23. В 15 с лишним раз больше. LLVM часть этого выбросит, но на opt-level = "z" или в толстых async-графах вызовов он быстро сдаётся.</p><h2>Четыре места, где компилятор работает вхолостую</h2><h3>1. Panic в состоянии Returned</h3><p>Контракт future требует только отсутствия UB. Паниковать при повторном poll — не обязательно. Можно просто вернуть Pending снова: ничего опасного не произойдёт, а ветка panic — это лишний побочный эффект, который плохо оптимизируется.</p><p>Дион сделал экспериментальный патч rustc: в release-режиме Returned больше не паникует. На embedded-прошивках это дало <b>2–5% экономии бинарного размера</b>. В debug-сборках панику можно оставить, чтобы быстро ловить ошибочный повторный poll, — по аналогии с overflow-checks.</p><h3>2. State machine без await</h3><p>Взгляните на функцию без единой await-точки:</p><p>Идеальная реализация — всегда возвращать Poll::Ready(5) безо всякого enum. А rustc генерирует полноценный CoroutineLayout с тремя служебными состояниями и switch по дискриминанту. Такие функции часто появляются в трейтах, где один интерфейс должен быть async, но конкретная реализация ничего не ждёт. Патч, убирающий state machine у async-блоков без await, даёт ещё <b>0,2%</b> размера — мало, но правка тривиальная.</p><h3>3. Futures не инлайнятся через await</h3><p>Классический шаблон: адаптер просто пересылает вызов вниз.</p><p>Сейчас bar получает собственный state machine, который вызывает state machine foo. Вручную мы бы написали fn bar(blah) -&gt; impl Future { foo(blah) } и избавились бы от лишнего enum. Аналогично с преамбулой и постамбулой: их можно перенести на FutureExt::map из крейта futures.</p><h3>4. Одинаковые состояния не схлопываются</h3><p>Если в async-функции несколько веток match ведут к одинаковому await, компилятор создаёт отдельное состояние на каждую ветку:</p><p>Здесь send_response вызывается с разными аргументами, но структура состояний дублируется. Если вынести выбор аргумента за await, получится одно состояние вместо двух:</p><p>В одном примере из статьи MIR сокращается с 456 до 302 строк, а ассемблер — примерно на 11%. Оптимизации stack, поэтому выигрыш в реальном коде может быть заметнее.</p><h2>Что делать сегодня, не дожидаясь rustc</h2><p>Всё перечисленное выше — это работа для команды компилятора. Но релиз с этими патчами может занять месяцы, а то и годы. Пока он не вышел, можно снизить async-bloat в своём коде.</p><p><b>Главный принцип:</b> каждый лишний async fn — это лишний state machine. Если функция не содержит await, не делайте её async.</p><h3>Заменяйте async-пустышки на impl Future</h3><p>Типичный случай — трейт, где одна реализация реально ждёт ввода-вывода, а другая просто возвращает значение.</p><p>Вместо полноценного state machine получается готовая future, которая сразу возвращает Ready. Это особенно полезно в embedded-HAL, где трейты часто async, но конкретный драйвер может делать только прямой доступ к регистрам.</p><h3>Прозрачные обёртки без await</h3><p>Если функция только пересылает await вниз, уберите await:</p><h3>Схлопывайте await-точки</h3><p>Если несколько веток match заканчиваются одним и тем же await, вынесите выбор аргументов наружу. Это уменьшает число состояний state machine и облегчает жизнь LLVM.</p><h3>Передавайте большие данные по ссылке</h3><p>Async-функция захватывает в state machine всё, что живёт через await. Если передать массив по значению, он окажется внутри future целиком:</p><p>Разница в 52 раза по размеру future — и это без учёта memcpy, который компилятор вынужден вставлять при перемещении больших значений.</p><h2>Перспективы: Project Goal и финансирование</h2><p>Дион оформил эти идеи как <a href="https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html" rel="noopener">Rust Project Goal</a> — формальный механизм, через который команды заявляют цели на полугодие. По его оценке, работа требует порядка <b>€30 000</b> финансирования. Для компиляторного проекта это скромная сумма: речь идёт о 2–5% размера прошивки практически в любом async-проекте.</p><p>Есть и другой подход к той же проблеме: не убирать state machine на входе, а научить LLVM лучше их оптимизировать на выходе. Дион считает, что оба направления дополняют друг друга. Чем проще state machine попадает в LLVM, тем эффективнее её можно проинлайнить и упростить на поздних проходах.</p><h2>Выводы</h2><blockquote>Async Rust never left the MVP state. The compiler generates state machines with a lot of unnecessary baggage. With a few targeted optimizations in rustc we can get smaller binaries and better performance for everyone.</blockquote><p>Утверждение «async Rust так и не вышел из MVP» звучит резко, но поясняет, почему наши «zero-cost» абстракции иногда всё-таки стоят дорого. Компилятор делает корректный, но не оптимальный код: лишние panic-ветки, неинлайненные futures, дублирующиеся состояния и state machine там, где они не нужны.</p><p>Для embedded и WASM это не абстрактная проблема, а конкретные килобайты прошивки. Пока rustc учится, разработчик может снизить налог вручную: убирать async у функций без await, возвращать impl Future из прозрачных обёрток, схлопывать await-точки и не передавать большие структуры по значению.</p><p>Если ваш проект на Rust бьётся о лимит flash, имеет смысл посмотреть на async-граф вызовов свежим взглядом. Часто проще убрать одну лишнюю async-обёртку, чем месяцами ждать патч в компиляторе.</p><p><b>Источники:</b><br />— <a href="https://tweedegolf.nl/en/blog/237/async-rust-never-left-the-mvp-state" rel="noopener">Dion Dokter, «Async Rust never left the MVP state»</a>;<br />— <a href="https://tweedegolf.nl/en/blog/235/debloat-your-async-rust" rel="noopener">Dion Dokter, «Debloat your async Rust»</a>;<br />— <a href="https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html" rel="noopener">Rust Project Goal: Async state machine optimisation</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Протоколы связи микроконтроллеров: проектирование и парсинг</title>
      <link>https://tproger.ru/articles/protokoly-svyazi-mikrokontrollerov-proektirovanie-i-parsing</link>
      <comments>https://tproger.ru/articles/protokoly-svyazi-mikrokontrollerov-proektirovanie-i-parsing?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Евгений Стребков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/protokoly-svyazi-mikrokontrollerov-proektirovanie-i-parsing</guid>
      <description><![CDATA[<p>Разбираем структуру кадра, контрольные суммы и конечные автоматы для обмена данными между хостом и микроконтроллером. Примеры на C — читайте и внедряйте.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/protokoly-svyazi-mikrokontrollerov-proektirovanie-i-parsing">Протоколы связи микроконтроллеров: проектирование и парсинг</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Оптимизация]]></category>
      <category><![CDATA[Сетевые протоколы]]></category>
      <category><![CDATA[Hardware]]></category>
      <category><![CDATA[Разработка]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Tue, 09 Jun 2026 07:25:27 GMT</pubDate>
      <content:encoded><![CDATA[<p>Протокол связи микроконтроллера — это набор правил формирования кадров поверх физического интерфейса (UART, RS-485). Если вы когда-либо писали прошивку, которой нужно «разговаривать» с компьютером, то знаете: надёжность связи важнее скорости. Один потерянный байт или сбой в синхронизации — и устройство зависает или выполняет чужую команду. В этой статье разберём, как проектировать собственные протоколы связи микроконтроллеров: от структуры кадра до парсера на конечном автомате.</p><h2>Что такое протокол передачи данных</h2><p>Под <b>протоколом передачи данных</b> здесь понимается формат пакетов (кадров), которые строятся поверх физического уровня. Физический уровень — это уже выбранный интерфейс: RS-232, RS-485, инфракрасный канал, радиомодуль или оптоволокно. От него мы получаем две базовые операции: отправить один байт и принять один байт. Всё остальное — наша задача.</p><p>Данные передаются пакетами — <b>кадрами</b>. Хороший кадр позволяет приёмнику понять: где начало сообщения, кому оно адресовано, сколько в нём полезных данных и не повредились ли они по дороге.</p><p>Кадр состоит из заголовка, адресов, типа данных, длины, полезной нагрузки, контрольной суммы и концевика.</p><p>Для защиты от случайных совпадений заголовок делают многобайтовым, а контрольную сумму считают по всей значащей части кадра.</p><p>Парсинг удобно реализовывать конечным автоматом: каждый принятый байт переводит машину в новое состояние.</p><p>На хосте данные почти всегда буферизуются; на простых МК часто выгоднее передавать напрямую, чтобы сэкономить ОЗУ.</p><h2>Структура кадра: из чего собирать пакет</h2><p>Надёжный протокол обычно включает семь логических полей. Не все обязательны — выбор зависит от задачи.</p><h3>Заголовок и концевик</h3><p><b>Заголовок</b> (frame header) и <b>концевик</b> (frame tail) отмечают границы кадра. Главное требование — минимизировать вероятность случайного совпадения этих байтов в потоке данных. Есть два подхода:</p><ul><li><b>Подбор характерных байтов.</b> Если данные предсказуемы (например, только ASCII-текст), можно выбрать заголовок из диапазона непечатаемых символов.</li><li><b>Увеличение длины.</b> Для случайных данных лучше сделать заголовок многобайтовым — например, 0x55 0xAA 0x7E. Вероятность случайного совпадения трёх конкретных байтов подряд падает экспоненциально. Даже если совпадение произойдёт, его отловит контрольная сумма.</li></ul><h3>Адресация</h3><p><b>Адрес назначения</b> нужен в системах типа «один к многим» — например, когда один хост управляет несколькими датчиками по общей шине RS-485. В сложных сетях добавляют ещё и <b>адрес источника</b>, чтобы получатель знал, от кого пришёл пакет.</p><h3>Тип, длина и данные</h3><p><b>Тип данных</b> (data type) говорит, что дальше идёт команда или полезная нагрузка. <b>Длина</b> (data length) указывает число значащих байтов в блоке данных. Вместе они образуют «тело» кадра — ту часть, которую мы действительно хотим доставить.</p><h3>Контрольная сумма</h3><p><b>Контрольная сумма</b> проверяет целостность кадра. Простейший вариант — арифметическая сумма всех байтов тела. Для более серьёзной защиты применяют CRC (циклический избыточный код): он ловит пакетные ошибки и перестановки битов, которые простая сумма пропустит. Выбор алгоритма — компромисс между скоростью вычисления на МК и требуемой надёжностью.</p><h2>Передача: хост и микроконтроллер</h2><p>На физическом уровне отправка сводится к посылке байтов один за другим. Но способ организации этой посылки сильно влияет на производительность.</p><h3>Передача с микроконтроллера</h3><p>На простых контроллерах вроде семейства 8051 часто используют <b>прямую передачу</b>: процессор загружает байт в буфер UART и ждёт флага готовности. Плюс — данные моментально оказываются на линии. Минус — процессор занят на всё время отправки.</p><p>Альтернатива — <b>передача по прерыванию</b>: байты складываются в кольцевой буфер, а прерывание UART отправляет их фоном. Экономит процессорное время, но требует ОЗУ под буфер. На 8051 с его скудной памятью прямая передача часто выигрывает.</p><h3>Передача с хоста</h3><p>На ПК данные почти всегда буферизуются операционной системой. Программист работает с тремя уровнями абстракции:</p><ol><li><b>Контролы ОС.</b> В Windows — компоненты вроде MSComm (устаревший 32-bit ActiveX, не рекомендуется для новых проектов). Просто, но нужно следить за блокировками при приёме и многопоточностью.</li><li><b>Системные API.</b> В Windows и Linux последовательный порт — это файл. Открываем через CreateFile или open(), но перед чтением и записью требуется настройка параметров порта (в Linux — termios со скоростью, чётностью и размером слова).</li><li><b>Класс-обёртка.</b> Например, CSerialPort для Windows: он инкапсулирует инициализацию, поток приёма и отправку. После открытия порта вызов WriteToPort отправляет массив байтов, а внутренний поток следит за входящими данными и шлёт сообщения родительскому окну.</li></ol><h2>Приём и парсинг на конечном автомате</h2><p>Приём данных на стороне микроконтроллера тоже бывает двух видов: <b>опрос</b> (поллинг) и <b>прерывание</b>. Опрос проще, но отнимает процессорное время. Прерывание эффективнее: байт пришёл — сработала процедура обработки прерывания (ISR, Interrupt Service Routine), процессор отвлёкся на доли миллисекунды и вернулся к задаче.</p><p>Где размещать парсер протокола? Если протокол простой, его можно держать прямо в обработчике прерывания: приняли корректный кадр — установили флаг, основной цикл реагирует. Для сложных протоколов лучше писать байты в буфер и разбирать их в главном цикле. Гибридный подход тоже возможен: в прерывании ищем только «команду подключения», а остальное парсим фоном.</p><h3>Пример: разбор кадра по состояниям</h3><p>Рассмотрим конкретный формат кадра:</p><p>Парсер реализуем через переменную state_machine. Каждое состояние соответствует ожидаемому байту. Если пришёл не тот байт — автомат сбрасывается в ноль. Это защищает от «залипания» в промежуточном состоянии при обрыве связи или помехах.</p><p><b>Совет по стабильности:</b><br />Сброс автомата при несовпадении заголовка, адресов, длины, checksum или концевика — ключевая техника. Без неё при обрыве кадра парсер застрянет в промежуточном состоянии, и следующие кадры будут отвергнуты.</p><h2>Приём на стороне хоста</h2><p>На ПК приём организуется проще: операционная система уже буферизует входящие байты. Для неблокирующего чтения запускают отдельный поток, который ждёт данных из порта и передаёт их в основной процесс через сообщения или обратный вызов. Класс CSerialPort, например, отправляет родительскому окну сообщение WM_COMM_RXCHAR с каждым новым байтом. Обработчик этого сообщения просто вызывает тот же парсер, что и на микроконтроллере.</p><p>Таким образом, логика разбора протокола <b>единая</b> для обеих сторон. Различается только способ доставки байтов в парсер: прерывание ISR на МК и поток ОС на хосте.</p><h2>Выводы</h2><p>Проектирование протокола связи для микроконтроллера — это не ракетостроение, но требует дисциплины. Хороший кадр защищает границы (заголовок + концевик), адресует получателя, указывает длину и проверяет целостность. Парсинг на конечном автомате делает код предсказуемым и устойчивым к сбоям.</p><p>Главный принцип — <b>сбрасывать автомат при любом нарушении ожидаемого шаблона</b>. Это предотвращает «залипание» и позволяет системе быстро восстановиться после помехи. На основе этой базы можно наращивать надёжность: добавлять повторные передачи, порядковые номера, шифрование — в зависимости от требований проекта.</p><blockquote>Хороший протокол — это не тот, который передаёт быстрее всех, а тот, который не ломается при помехах.</blockquote><p><b>Источник:</b> оригинальная статья Leo Liu — <a href="https://dev.to/sienovoleo/microcontroller-communication-protocol-design-1e29">Microcontroller Communication Protocol Design</a> (DEV Community).</p>]]></content:encoded>
    </item>
    <item>
      <title>Пишем прошивку Bluetooth Low Energy на Zephyr OS: полное руководство для разработчиков</title>
      <link>https://tproger.ru/articles/piwem-prowivku-bluetooth-low-energy-na-zephyr-os-polnyj-gajd-dl</link>
      <comments>https://tproger.ru/articles/piwem-prowivku-bluetooth-low-energy-na-zephyr-os-polnyj-gajd-dl?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Евгений Стребков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/piwem-prowivku-bluetooth-low-energy-na-zephyr-os-polnyj-gajd-dl</guid>
      <description><![CDATA[<p>Как с нуля собрать BLE-устройство на Zephyr OS: настройка окружения, GAP, GATT, notifications и примеры кода. Практический гайд для embedded-разработчиков. Попробуйте повторить на nRF52840!</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/piwem-prowivku-bluetooth-low-energy-na-zephyr-os-polnyj-gajd-dl">Пишем прошивку Bluetooth Low Energy на Zephyr OS: полное руководство для разработчиков</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Open Source]]></category>
      <category><![CDATA[Инструменты]]></category>
      <category><![CDATA[Сетевые протоколы]]></category>
      <category><![CDATA[Hardware]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 04 Jun 2026 10:45:38 GMT</pubDate>
      <content:encoded><![CDATA[<p>Ваши беспроводные наушники только что подключились к телефону, смарт-часы синхронизировали пульс, а датчик температуры в соседней комнате отправил показания на шлюз. Всё это работает через Bluetooth Low Energy, и всё чаще прошивку под капотом таких устройств пишут на Zephyr OS.</p><p>Если вы разрабатываете встраиваемые системы или хотите войти в IoT, этот материал — практическое руководство по созданию BLE-периферии с нуля. Мы разберём, как устроен стек, как настроить окружение и как написать код, который управляет железом со смартфона.</p><h2>Что такое Zephyr OS и зачем она для BLE</h2><p><a href="https://zephyrproject.org/">Zephyr OS</a> — открытая операционная система реального времени под управлением Linux Foundation. Лицензия Apache 2.0, поддержка более 600 плат на ARM, RISC-V, x86 и других архитектурах. Главное для нас: внутри неё живёт полноценный BLE-стек, прошедший официальную сертификацию Bluetooth SIG. Рекомендуем также обзор <a href="https://tproger.ru/articles/pervyj-risc-v-rva23-chip-vhodit-v-mainline-linux-chto-eto-znachit/">«Первый RISC-V RVA23 чип входит в mainline Linux»</a>.</p><p>Zephyr OS включает сертифицированный BLE-стек с открытым кодом — от приложения до радиоконтроллера.</p><p>GAP управляет обнаружением и соединениями, GATT — структурой данных (сервисы и характеристики).</p><p>Для старта понадобятся плата nRF52840 DK (~40 USD) и смартфон с приложением nRF Connect.</p><p>Прошивка собирается через west — единый инструмент для клонирования, сборки и прошивки.</p><p>Оптимизация энергопотребления: интервал advertising, peripheral latency, System OFF и отключение неиспользуемых периферий.</p><p>Это означает, что вы работаете не с хобби-реализацией, а со стеком, который прошёл conformance-тестирование. От прикладного кода до радиорегистров — всё открыто. Никаких бинарных блобов. Nordic Semiconductor, чьи чипы доминируют на рынке BLE, построила свой nRF Connect SDK поверх Zephyr. Когда инженеры Nordic пишут прошивку, они используют Zephyr.</p><p>Стек поддерживает Bluetooth 5.x (2M PHY, Coded PHY для дальней связи, Extended Advertising), Bluetooth Mesh, LE Audio с кодеком LC3, Direction Finding и все стандартные профили. Если вы делаете BLE-продукт в 2025–2026 годах — Zephyr входит в тройку лучших платформ.</p><h2>Как работает BLE: два слоя, которые нужно знать</h2><p>Прежде чем писать код, нужна ментальная модель. BLE — это не «классический» Bluetooth для аудио. Он создан для коротких, редких сеансов связи: датчик проснулся, отправил 20 байт, уснул. Такое устройство может работать от таблеточной батарейки годами.</p><p>Разработчик взаимодействует с двумя слоями:</p><ul><li><b>GAP</b> (Generic Access Profile) — отвечает за обнаружение и соединения. Периферия широковещательно рассылает пакеты (advertising), а central (обычно телефон) их слушает и инициирует подключение.</li><li><b>GATT</b> (Generic Attribute Profile) — отвечает за обмен данными после соединения. Данные организованы в иерархии: устройство предоставляет <b>сервисы</b>, внутри которых находятся <b>характеристики</b> — единицы данных со свойствами Read, Write, Notify, Indicate.</li></ul><p>Каждый сервис и характеристика идентифицируются UUID. Стандартные (от Bluetooth SIG) — 16-битные, пользовательские — 128-битные. Хорошо спроектированный набор сервисов — это API вашего устройства.</p><h2>Ключевые выводы</h2><h2>Настройка окружения</h2><p>Для работы понадобится Linux (Ubuntu 22.04+), macOS или Windows с WSL2. Идеальная плата для старта — <a href="https://www.nordicsemi.com/Products/Development-hardware/nRF52840-DK">Nordic nRF52840 DK</a>. Она недорогая, широко доступна и имеет встроенный отладчик J-Link. А ещё посмотрите, <a href="https://tproger.ru/articles/kak-sozdat-samodelnye-ustrojstva-na-raspberry-pi-esp32-i-node-red/">как создать самодельные устройства на Raspberry Pi, ESP32 и Node-RED</a>.</p><h3>Установка зависимостей</h3><p>На Ubuntu устанавливаем toolchain и утилиты:</p><p>Устанавливаем west — CLI для управления многорепозиторным workspace:</p><p>Инициализируем workspace и скачиваем модули (HAL-ы, криптобиблиотеки, Bluetooth controller):</p><p>Устанавливаем Python-зависимости и SDK с кросс-компиляторами:</p><p>Добавляем в ~/.bashrc:</p><h2>Первое приложение: простой beacon</h2><p>Начнём с минимального примера: устройство, которое только объявляет о себе по радио и не принимает соединений. Это база для iBeacon, Eddystone и отслеживание активов.</p><p>Создаём структуру проекта:</p><p>Файл CMakeLists.txt:</p><p>Конфигурация prj.conf:</p><p>Код src/main.c:</p><p>Что происходит: bt_enable(NULL) инициализирует стек синхронно. bt_le_adv_start с параметром BT_LE_ADV_NCONN запускает неконнективный advertising. Массив ad содержит флаги (general discoverable, no BR/EDR) и имя устройства.</p><p>Собираем и прошиваем:</p><p>Открываем nRF Connect на смартфоне, сканируем и видим «MyBeacon» в списке устройств. Прошивка работает.</p><h2>Кастомный сервис: управляем LED со смартфона</h2><p>Beacon полезен, но скучен. Настоящая магия начинается, когда телефон подключается к устройству и управляет им. Сделаем сервис, который позволяет включать LED на плате и читать состояние кнопки.</p><p>Конфигурация prj.conf:</p><p>Основной код. Здесь определяем пользовательские 128-битные UUID для сервиса и двух характеристик:</p><p>Ключевые моменты: BT_GATT_SERVICE_DEFINE собирает GATT базу данных на этапе компиляции. Характеристика LED доступна для чтения и записи, а кнопка — только для чтения. UUID сервиса вынесен в scan response, чтобы не перегружать 31-байтовый advertising-пакет.</p><p>После прошивки подключаемся через nRF Connect, находим кастомный сервис (UUID начинается с 00001234), записываем 0x01 в LED-характеристику — светодиод загорается. Только что вы управляли железом со смартфона по Bluetooth.</p><h2>Notifications: отправляем данные без запроса</h2><p>Чтение и запись — это pull-модель. Но датчики обычно работают по push: термометр сам шлёт температуру, пульсомер — BPM. В BLE для этого есть notifications.</p><p>Чтобы добавить notification в характеристику, нужно:</p><ol><li>Указать свойство BT_GATT_CHRC_NOTIFY в характеристике.</li><li>Добавить CCCD (Client Characteristic Configuration Descriptor) через BT_GATT_CCC — телефон пишет в него 0x0001, чтобы включить уведомления.</li><li>Вызвать bt_gatt_notify при изменении данных.</li></ol><p>Типовой паттерн — периодическая отправка через delayable work item:</p><p>Work item выполняется в системном потоке, а не в контексте прерывания, поэтому вызовы Bluetooth API безопасны.</p><h2>Полный sensor node в одном файле</h2><p>Соберём всё вместе: датчик температуры (симулированный), который читается по запросу и пушится через notifications с настраиваемым интервалом. Добавим обработку соединений и LED-индикацию статуса.</p><p>Здесь важны две детали. Во-первых, температура хранится в формате fixed-point (сотые доли градуса в int16_t) — это позволяет избежать дорогих операций с плавающей точкой на микроконтроллерах без FPU. Во-вторых, проверка в функции записи: если телефон шлёт некорректный интервал, стек возвращает ошибку BT_ATT_ERR_VALUE_NOT_ALLOWED.</p><p>При разрыве соединения обработчики отменяют work item, гасят LED и перезапускают advertising — устройство снова доступно для обнаружения.</p><h2>Безопасность: pairing и шифрование</h2><p>В рабочей среде BLE без защиты — это устройство, которое любой прохожий может переключить или прочитать. Pairing создаёт зашифрованный канал и, опционально, аутентифицирует стороны.</p><p>Методы pairing в BLE:</p><ul><li><b>Just Works</b> — шифрование без аутентификации. Защищает от пассивного прослушивания, но не от MITM.</li><li><b>Passkey Entry</b> — пользователь вводит 6-значный код. Аутентификация есть.</li><li><b>Numeric Comparison</b> — оба устройства показывают число, пользователь подтверждает совпадение.</li><li><b>Out of Band (OOB)</b> — обмен ключами через внешний канал, например NFC.</li></ul><p>Включаем SMP (Security Manager Protocol) и persistent storage для bonding:</p><p>Данные сопряжения (ключи, обменянные при pairing) сохраняются во флеш-памяти и переживают перезагрузку. Без этого пользователю придётся проходить pairing после каждого включения устройства — ужасный пользовательский опыт.</p><p>Чтобы требовать шифрования для чтения характеристики, меняем permission:</p><p>BT_GATT_PERM_READ_ENCRYPT означает, что чтение возможно только по зашифрованному соединению. Если телефон пытается прочитать без pairing, стек автоматически инициирует процедуру сопряжения.</p><h2>Роль central: сканируем и подключаемся</h2><p>Все примеры выше — периферия (peripheral): устройство, которое рассылает advertising и ждёт подключения. Другая сторона — central: шлюз, хаб, сборщик данных. Для полной картины нужно понимать обе роли.</p><p>Конфигурация central:</p><p>Запуск сканирования и подключение к первому найденному устройству с RSSI выше -70 dBm:</p><p>После подключения выполняется обнаружение сервисов через bt_gatt_discover, затем — чтение характеристик и подписка на notifications через bt_gatt_subscribe.</p><h2>Оптимизация: MTU, PHY и энергопотребление</h2><p>По умолчанию BLE ATT MTU — 23 байта. Минус 3 байта заголовка ATT, получаем 20 байт полезной нагрузки на операцию. Для передачи прошивки или больших буферов этого мало.</p><p>Включаем MTU 247 байт и Data Length Extension:</p><p>Zephyr автоматически инициирует MTU exchange при соединении. С 247-байтным MTU, интервалом 7,5 мс и 2M PHY пропускная способность достигает ~800–1000 кбит/с на практике.</p><p>Выбор PHY — ещё один рычаг:</p><ul><li><b>1M PHY</b> — дефолт, стандартная дальность и скорость.</li><li><b>2M PHY</b> (Bluetooth 5.0) — удваивает скорость, сокращает время работы радио и экономит энергию. Дальность чуть меньше.</li><li><b>Coded PHY</b> — увеличивает дальность в 2–4 раза за счёт помехоустойчивого кодирования, но скорость падает до 125 кбит/с.</li></ul><p>Энергосбережение — ключ к автономным устройствам. Главные приёмы:</p><ol><li>Увеличить advertising interval: 1000 мс потребляет в 5 раз меньше, чем 200 мс.</li><li>Использовать peripheral latency: пропускать connection events, когда нечего передавать.</li><li>Включить System OFF: ток nRF52840 падает с ~3 мА до ~1,5 мкА.</li><li>Отключить неиспользуемые периферии в devicetree overlay — UART, SPI, I2C жрут ток даже в простое.</li></ol><p>Цель для BLE-датчика — средний ток в единицы микроампер. При таком потреблении таблеточная батарейка CR2032 (225 мА·ч) проработает годы.</p><h2>Что ещё умеет стек: Mesh, DFU и LE Audio</h2><p>Помимо классической point-to-point связи, Zephyr поддерживает три продвинутые технологии:</p><h3>Bluetooth Mesh</h3><p><b>Bluetooth Mesh</b> — многие-ко-многим сеть поверх BLE. Узлы пересылают сообщения друг другу, расширяя радиус действия за пределы одного соединения. Используется в умных домах и офисах: десятки лампочек, выключателей и датчиков общаются без единого центрального хаба.</p><h3>DFU over BLE</h3><p><b>DFU over BLE</b> — обновление прошивки по воздуху. Интеграция с MCUboot даёт два слота: активный и резервный. Новая прошивка загружается через BLE SMP сервис, устройство перезагружается, MCUboot проверяет подпись и, в случае неудачи, автоматически откатывается к предыдущей версии.</p><h3>LE Audio</h3><p><b>LE Audio</b> — новый аудиостандарт на базе BLE. Кодек LC3 даёт более высокое качество, чем классический SBC, при вдвое меньшем битрейте. Режим BIS (broadcast) позволяет одному источнику транслировать звук на неограниченное число приёмников — технология, лежащая в основе <a href="https://www.bluetooth.com/auracast/">Auracast</a>.</p><h2>Отладка: что делать, когда не работает</h2><p>BLE сложно отлаживать, потому что радио невидимо. Начинайте с логов:</p><p>Это выдаст подробную трассировку каждой HCI-команды, advertising event и GATT-операции. Вывод идёт в UART — не оставляйте UART включённым в рабочей среде, он жрёт ток.</p><p>Интерактивный Zephyr shell позволяет вручную запускать bt advertise on, bt gatt discover и bt gatt read через последовательный порт. Это бесценно для быстрой проверки гипотез без перепрошивки.</p><p>Для анализа радиоэфира используйте nRF Sniffer для Bluetooth LE — прошивка для nRF52840 DK, которая вместе с Wireshark показывает каждый пакет в эфире: advertising PDU, connection events, pairing exchange. Это окончательный инструмент, когда подозреваете проблему на уровне протокола.</p><h2>Часто задаваемые вопросы</h2><h2>Выводы</h2><p>Zephyr OS превращает разработку BLE-устройств из работы с чёрными ящиками в инженерную дисциплину. Полный открытый стек, сертификация Bluetooth SIG, поддержка 600+ плат и активное сообщество делают его одной из сильнейших платформ для IoT.</p><p>Мы прошли путь от простого beacon до полноценного sensor node с notifications, управлением соединениями и проверкой входных данных. Этих паттернов — advertising, GATT-обработчики, CCCD, подсчёт ссылок на соединение — достаточно, чтобы построить практически любое BLE-периферийное устройство.</p><p>Следующий шаг — взять реальное железо, реальный датчик и собрать устройство, которое решает вашу задачу. Термометр для теплицы, беспроводная кнопка для умного дома, трекер для велосипеда — всё это становится доступным после освоения базовых паттернов.</p><blockquote>Главный способ понять BLE — не читать спецификацию, а заставить мигать LED со смартфона. Когда это получится, остальное пойдёт быстро.</blockquote><p><b>Источники:</b></p><p><a href="https://www.freecodecamp.org/news/how-to-build-bluetooth-applications-with-zephyr-os-a-handbook-for-devs/">How to Build Bluetooth Applications with Zephyr OS: A Handbook for Devs</a> — freeCodeCamp.</p><p><a href="https://docs.zephyrproject.org/latest/connectivity/bluetooth/index.html">Zephyr Bluetooth Stack Documentation</a> — официальная документация.</p><p><a href="https://www.bluetooth.com/specifications/">Bluetooth SIG Specifications</a> — спецификации протоколов и профилей.</p>]]></content:encoded>
    </item>
    <item>
      <title>Когда компилятор врёт: нарушение memory safety в safe-коде Go</title>
      <link>https://tproger.ru/translations/kogda-kompilyator-vryot-naruwenie-memory-safety-v-safe-kode-go</link>
      <comments>https://tproger.ru/translations/kogda-kompilyator-vryot-naruwenie-memory-safety-v-safe-kode-go?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Евгений Стребков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/kogda-kompilyator-vryot-naruwenie-memory-safety-v-safe-kode-go</guid>
      <description><![CDATA[<p>CVE-2026-27143 и CVE-2026-27144: два бага Go-компилятора ломали memory safety без unsafe, CGO и data races. Разбор механизма, примеры кода, исправление в Go 1.26.1.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/kogda-kompilyator-vryot-naruwenie-memory-safety-v-safe-kode-go">Когда компилятор врёт: нарушение memory safety в safe-коде Go</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Безопасный код]]></category>
      <category><![CDATA[Языки программирования]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 08 Apr 2026 10:30:03 GMT</pubDate>
      <content:encoded><![CDATA[<p>Если вы пишете на Go и считаете, что safe-код автоматически защищает от memory corruption — вот два реальных CVE, которые доказывают обратное: баги компилятора Go (исправлены в 1.26.1) позволяли получить control-flow hijack и повреждение памяти <em>без единого</em> unsafe, CGO, ассемблерной вставки и data race.</p><p>Исследователь Доминик Цёлек (<a href="https://ciolek.dev">ciolek.dev</a>) обнаружил два бага в цепочке компиляции Go и сообщил о них команде безопасности. Та ответила на первый отчёт через 3 минуты, на второй — через 4. Что показательно: git blame указал на самого исследователя как автора кода, в котором обнаружился баг, — он же сам написал его три года назад.</p><ul><li>CVE-2026-27143: переполнение int8-счётчика в цикле → prove pass удалял bounds check → инъекция инструкций</li><li>CVE-2026-27144: no-op-конверсия *p = T(*q) вместо *p = *q → компилятор пропускал careful copy path при пересечении памяти → memory corruption</li><li>Общая причина: «counterfeit certainty» — оптимизатор повышал «вероятно безопасно» до «доказано безопасно» слишком рано</li><li>Memory safety — свойство всей цепочки инструментов (frontend, optimizer, lowering, runtime, codegen), а не только языка</li><li>«Every optimization is a security claim» — каждая оптимизация компилятора является утверждением о безопасности</li></ul><h2>Баг 1 (CVE-2026-27143): переполнение счётчика цикла убирает bounds check</h2><p>Первый баг связан с фазой <em>prove pass</em> — проходом компилятора, который статически доказывает инварианты и на основании этого удаляет избыточные проверки. Идея хорошая: зачем проверять границы массива, если компилятор «знает», что индекс всегда находится в допустимом диапазоне?</p><p>Рассмотрим цикл с переменной типа int8, шагом 10 и лимитом 120:</p><p>На первый взгляд код безопасен: i не превышает 120, массив размером 200 байт — никакого выхода за границы. Prove pass рассуждал именно так: раз условие цикла гарантирует i ∈ [0, 120], bounds check можно удалить.</p><p>Но int8 хранит значения от -128 до 127. После 120 следующий шаг — 130, что переполняет тип и оборачивается в -126. Условие i &lt; 120 снова становится истинным, цикл продолжается, но уже с отрицательным индексом. Bounds check, который мог бы это поймать, компилятор уже выбросил — ведь он «доказал» безопасность. В результате происходит запись за пределы массива, что открывает возможность для control-flow hijack и инъекции инструкций.</p><blockquote>Prove pass повысил «индекс вероятно в пределах диапазона» до «индекс доказано в пределах диапазона», не учтя возможность целочисленного переполнения малых типов.</blockquote><h2>Баг 2 (CVE-2026-27144): no-op-конверсия и пересечение памяти</h2><p>Второй баг тоньше. Go использует «careful copy path» — специальный путь копирования данных при работе с пересекающимися областями памяти (overlapping memory). Без него можно записать в destination данные, которые уже были перезаписаны в процессе копирования из source.</p><p>Проблема возникала при казалось бы безобидной конструкции — no-op-конверсии типа при присваивании через указатель:</p><p>Конверсия T(*q) здесь не делает ничего содержательного — типы совпадают. Компилятор распознал её как no-op и оптимизировал… но при этом забыл переключиться на careful copy path. Если области памяти p и q пересекаются — данные копируются некорректно, что приводит к повреждению памяти.</p><p>Особенно опасным сценарием оказались slice values: заголовок слайса содержит указатель на данные, длину и ёмкость. При некорректном копировании заголовка указатель мог указывать на произвольную память, а длина — быть некорректной.</p><h2>Общая причина: «counterfeit certainty»</h2><p>Оба бага объединяет одна концептуальная проблема: компилятор принимал «вероятно безопасно» за «доказано безопасно» и на этом основании удалял защитные механизмы. Цёлек называет это <em>counterfeit certainty</em> — поддельная уверенность.</p><p>В первом случае prove pass «доказал» безопасность индекса, не учтя переполнение малых целых типов. Во втором случае оптимизатор «знал», что конверсия — no-op, и сделал из этого вывод, что careful copy path не нужен. В обоих случаях рассуждение было формально правильным, но неполным: не были рассмотрены все возможные состояния программы.</p><blockquote>Every optimization is a security claim. — Каждая оптимизация компилятора является утверждением о безопасности.</blockquote><p>Когда компилятор удаляет bounds check — он утверждает, что выход за границы невозможен. Когда он пропускает careful copy path — он утверждает, что пересечения памяти нет. Ошибка в этих утверждениях напрямую превращается в уязвимость безопасности.</p><h2>Memory safety — свойство всей цепочки инструментов</h2><p>Принято считать, что memory-safe языки (Go, Rust, Java, C#) гарантируют отсутствие memory corruption по определению. Это правда — в рамках спецификации языка. Но между спецификацией и работающей программой стоит компилятор.</p><p>Цепочка компиляции Go включает несколько фаз, каждая из которых делает предположения о корректности предыдущей:</p><ul><li>Frontend (парсинг, type checking) — проверяет синтаксис и типы</li><li>Optimizer (prove pass, escape analysis) — доказывает инварианты и устраняет лишние проверки</li><li>Lowering — преобразует высокоуровневые операции в машинно-зависимые</li><li>Runtime (garbage collector, goroutine scheduler) — управляет памятью во время выполнения</li><li>Codegen — генерирует итоговый машинный код</li></ul><p>CVE-2026-27143 затронул фазу optimizer, CVE-2026-27144 — фазу lowering. Обе уязвимости существовали несмотря на то, что исходный код был написан на «memory-safe» языке. Memory safety — это свойство, которое нужно обеспечивать на каждом уровне стека, не только на уровне языка.</p><h2>Go security team: ответ за 3–4 минуты</h2><p>Исследователь отправил отчёты об обоих багах в Go security team. Первый ответ пришёл через 3 минуты, второй — через 4. Оба бага были исправлены в Go 1.26.1. Сам Цёлек отметил, что процесс взаимодействия с командой был образцовым: быстрая реакция, прозрачная коммуникация и оперативный выпуск патча.</p><p>При разборе бага исследователь запустил git blame на подозрительный участок кода компилятора — и обнаружил собственное имя. Три года назад он сам написал этот код. По его словам, это добавило исследованию особой иронии и заставило более внимательно относиться к тому, что «очевидные» оптимизации несут в себе скрытые предположения о безопасности.</p><h2>Выводы</h2><p>История с CVE-2026-27143 и CVE-2026-27144 показывает: использование memory-safe языка — необходимое, но недостаточное условие для безопасности программы. Компилятор — не чёрный ящик с гарантиями, а сложная система с собственными инвариантами, которые могут нарушаться.</p><p>Оба бага существовали в «тихих» местах: оптимизаторе, который молча удалял проверки, и в lowering-фазе, которая молча пропускала защитный путь копирования. Ни один линтер, ни один статический анализатор не мог их обнаружить — ведь исходный код был совершенно корректным с точки зрения языка.</p><p>Практический вывод прост: обновляйтесь. Следите за <a href="https://groups.google.com/g/golang-announce">golang-announce</a>, обновляйтесь до новых версий Go сразу после выхода патчей безопасности, и помните, что «написано на Go» ≠ «memory-safe по определению».</p>]]></content:encoded>
    </item>
    <item>
      <title>Аппаратное сжатие текстур: AFRC, PVRIC4 и Metal</title>
      <link>https://tproger.ru/translations/apparatnoe-szhatie-tekstur--afrc--pvric4-i-metal</link>
      <comments>https://tproger.ru/translations/apparatnoe-szhatie-tekstur--afrc--pvric4-i-metal?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Алексей Михайлишин]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/apparatnoe-szhatie-tekstur--afrc--pvric4-i-metal</guid>
      <description><![CDATA[<p>Сравнение трёх форматов аппаратного сжатия текстур — ARM AFRC, ImgTec PVRIC4 и Apple Metal Lossy. Точные данные RMSE, производительность в МПикс/сек, Vulkan BPC.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/apparatnoe-szhatie-tekstur--afrc--pvric4-i-metal">Аппаратное сжатие текстур: AFRC, PVRIC4 и Metal</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Оптимизация]]></category>
      <category><![CDATA[Hardware]]></category>
      <category><![CDATA[Apple]]></category>
      <category><![CDATA[Компьютерная графика]]></category>
      <category><![CDATA[ARM]]></category>
      <category><![CDATA[GPU]]></category>
      <category><![CDATA[Алгоритмы сжатия]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Mon, 30 Mar 2026 15:22:10 GMT</pubDate>
      <content:encoded><![CDATA[<p>Перевод статьи <a href="https://www.ludicon.com/castano/blog/2026/03/hardware-image-compression/">Hardware Image Compression</a> Игнасио Кастаньо (Ignacio Castaño). Оригинал опубликован в марте 2026 года.</p><p>Одним из поводов для разочарования в области аппаратных форматов изображений всегда было медленное развитие. Разработчики обычно не решались поставлять текстуры в новом формате, пока он не становился повсеместно доступным — поддерживался большинством целевого железа и всеми вендорами без исключения. Сегодня три вендора предлагают собственные форматы аппаратного сжатия текстур на лету: ARM AFRC, ImgTec PVRIC4 и Apple Metal Lossy. Игнасио Кастаньо исследовал все три и сравнил их с программным сжатием реального времени Spark от NVIDIA.</p><p>— ARM AFRC (Pixel 8, Mali-G715) — явный победитель: по метрике RMSE значительно обходит Spark и других конкурентов во всех форматах.</p><p>— Apple Metal Lossy (A15/M2+) — коэффициент сжатия 1:2, API минималистичен: один флаг в дескрипторе текстуры.</p><p>— ImgTec PVRIC4 (Pixel 10) разочаровал: драйвер игнорирует запрошенный bitrate, качество хуже Spark для R и RG форматов.</p><p>— Vulkan-расширение VK_EXT_image_compression_control унифицирует доступ к аппаратному сжатию через параметр BPC (bits per component).</p><p>— Аппаратное сжатие — перспективная альтернатива программному, но пока ограничено современными топовыми устройствами.</p><h2>Контекст: почему это важно</h2><p>Разработчики обычно не решались поставлять текстуры в новом формате, пока он не становился широко доступным — то есть поддерживался большинством целевого железа и всеми вендорами. Например, хотя ATI представила форматы 3Dc в 2004 году вместе с Radeon X800 (R420) и открыла их через расширения D3D9, их использование не стало распространённым, когда Direct3D 10 стандартизировал их как BC4 и BC5 в 2007 году. Массовое применение началось лишь тогда, когда Direct3D 10 стал минимальным требованием к железу.</p><p><a href="https://www.crytek.com/games/crysis">Crysis</a> стала первой крупной игрой, поставляемой с BC5-текстурами, но большинство игр ещё долгие годы не решались устанавливать такое жёсткое требование к железу. Чтобы избежать задержек с принятием, форматы BC6 и BC7 разрабатывались совместно ATI и NVIDIA для Direct3D 11.</p><p>Именно поэтому сжатие текстур в реальном времени так интересно: когда кодировщик работает в реальном времени, внедрять новые аппаратные форматы значительно проще — не нужно ждать, пока будет подготовлен контент, целенаправленно созданный под них. Аппаратное сжатие устраняет эту проблему принятия: детали форматов не документируются, их использование абсолютно прозрачно — приложению не нужно явно указывать эти форматы, драйвер сжимает текстуры динамически в процессе рендеринга и загрузки изображений.</p><h2>Apple Metal: Lossy-сжатие</h2><p>Apple представила сжатие текстур с потерями в чипах A15 и M2 (оба используют одно поколение GPU). Оно обеспечивает коэффициент сжатия 1:2. Включить его на удивление просто — API минималистичен. Свойство compressionType дескриптора MTLTextureDescriptor принимает значение из перечисления MTLTextureCompressionType, и установка MTLTextureCompressionTypeLossy зачастую является единственным необходимым изменением:</p><p>Согласно таблицам возможностей Metal Feature Set Tables, все обычные пиксельные форматы поддерживают сжатие с потерями — включая 10-битные и форматы с плавающей точкой. Тестирование это подтвердило, однако основное внимание сосредоточено на форматах R8, RG8 и RGBA8.</p><p>Внутренний алгоритм Apple не задокументирован. По результатам реверс-инжиниринга lossy-форматов используется размер блока 8×4 пикселя — и они напоминают некоторые особенности форматов ETC и EAC. Несмотря на заявленное сжатие 1:2, на практике для каждого блока выделяется один байт метаданных, так что реальное потребление памяти чуть выше заявленного.</p><h3>Качество на M4 Pro</h3><p>Для форматов R и RG Metal Lossy показывает результаты лучше кодеков Spark EAC, но хуже BC4 и BC5. Результаты RMSE (Root Mean Square Error — среднеквадратичная ошибка; меньше = лучше):</p><p><b>Формат R:</b><br /><br /></p>МетрикаMetal Lossy (1:2)BC4 Medium (1:2)BC4 High (1:2)EAC_RG Low (1:2)EAC_RG Medium (1:2)EAC_RG High (1:2)RMSE1,85791,84691,71492,33992,29221,8636<p></p><p><b>Формат RG:</b><br /><br /></p>МетрикаMetal Lossy (1:2)BC5 Medium (1:2)BC5 High (1:2)EAC_RG Low (1:2)EAC_RG Medium (1:2)EAC_RG High (1:2)RMSE3,17573,30993,04424,22614,15923,3601<p></p><p>Прямое сравнение lossy RGBA8 с форматами Spark некорректно из-за разных коэффициентов сжатия: Metal Lossy поддерживает только 1:2, тогда как форматы Spark RGB(A) — 1:4. Тем не менее, для полноты картины:</p><p><b>Формат RGBA:</b><br /><br /></p>МетрикаMetal Lossy (1:2)ASTC 4×4 Low (1:4)ASTC 4×4 Medium (1:4)ASTC 4×4 High (1:4)BC7 Low (1:4)BC7 Medium (1:4)BC7 High (1:4)RMSE1,49476,29945,96865,36375,72135,35854,2136<p></p><h3>Производительность на M4 Pro</h3><p>С точки зрения производительности lossy-форматы показывают себя отлично и при достаточно большом размере текстуры упираются в пропускную способность памяти. Результаты на M4 Pro (16 ядер GPU) в МПикс/сек:</p><p></p>Метод409620481024512256Uncompressed (blit)41 61826 68043 74970 11144 939Metal Lossy (blit)41 80740 84743 10069 87348 729BC7 High (GPU)35 56342 23037 08234 22410 985<p></p><p>Обратите внимание, что пропускная способность стандартных блитов остаётся достаточно стабильной вне зависимости от размера текстуры. Кодеки Spark, напротив, имеют фиксированные накладные расходы, которые становятся заметнее при уменьшении размеров текстур. Интересен скачок скорости блитов при 512×512 — у него пока нет объяснения. Также стоит учесть, что кодеки Spark требуют дополнительного копирования из буфера вывода кодека в финальную сжатую текстуру — это дополнительные накладные расходы, которых можно было бы избежать, если бы Metal поддерживал запись в блочно-сжатые текстуры, как это делает Vulkan.</p><h2>Vulkan: расширение VK_EXT_image_compression_control</h2><p>В Vulkan расширение VK_EXT_image_compression_control даёт приложениям возможность запрашивать сжатие изображений с фиксированной скоростью. Расширение уже доступно на флагманских устройствах от ARM и Imagination. Включение потерявого сжатия в Vulkan несколько многословнее, чем в Metal, но на практике ненамного сложнее: нужно лишь расширить структуру VkImageCreateInfo, добавив в цепочку VkImageCompressionControlEXT.</p><p>Можно использовать флаг VK_IMAGE_COMPRESSION_FIXED_RATE_DEFAULT_EXT, чтобы позволить реализации самой выбрать параметры сжатия:</p><p>Альтернативно можно явно указать флаги фиксированной скорости для управления допустимыми коэффициентами сжатия:</p><p>BPC (bits per component — бит на компонент) — несколько нестандартная единица, но она позволяет задавать коэффициент сжатия единообразно вне зависимости от количества каналов. Для справки, BPC существующих форматов блочного сжатия GPU:</p><p></p>ФорматКаналыРазмер на пиксельРазмер на каналBC1RGB4 bpp~1,33 bpcBC4R4 bpp4 bpcBC5RG8 bpp4 bpcBC7RGBA8 bpp2 bpcASTC 4×4RGBA8 bpp2 bpcASTC 6×6RGBA~3,55 bpp~1,18 bpc<p></p><p>Расширение VK_EXT_image_compression_control также присутствует в некоторых драйверах AMD и Qualcomm, однако, насколько известно, ни один из этих вендоров не поддерживает аппаратное сжатие изображений с фиксированной скоростью. В случае AMD расширение присутствует в драйвере RADV, чтобы Proton мог отключать потерявое сжатие фреймбуфера в некоторых играх, где оно вызывало проблемы с корректностью — через флаг VK_IMAGE_COMPRESSION_DISABLED_EXT.</p><h2>ARM AFRC: лучший результат среди всех</h2><p>ARM Fixed Rate Compression (AFRC) было анонсировано в 2021 году и впервые появилось в Mali-G510 в 2022-м, однако широкого распространения этот дизайн не получил. По-настоящему массовым AFRC стало с выпуском Mali-G715 и Mali-G615 в том же году. Тестирование проводилось на Pixel 8 с GPU Mali-G715.</p><p>Устройство сообщило поддержку следующих форматов сжатия с фиксированной скоростью (возможности значительно шире Metal Lossy):</p><p></p>Формат2 bpc3 bpc4 bpc5 bpcR82 bpp3 bpp4 bpp—RG84 bpp6 bpp8 bpp—RGB86 bpp—12 bpp15 bppRGBA88 bpp12 bpp16 bpp—<p></p><p>В отличие от Metal Lossy, AFRC не использует дополнительных байт метаданных — все управляющие биты находятся внутри самого блока. Изображение делится на блоки 8×8 пикселей, в ряде случаев разбиваемые на подблоки меньшего размера. Размер каждого блока 8×8 в байтах:</p><p></p>Формат2 bpc3 bpc4 bpc5 bpcR8162432—RG8324864—RGB864—96128RGBA86496128—<p></p><p>По результатам реверс-инжиниринга удалось установить: AFRC представляет цвета с использованием преобразования YCoCg, а представление пикселей напоминает вейвлет Хаара. Для каждого подблока 4×4 используется 16 коэффициентов, квантование которых зависит от режима. Форматы RGB и RGBA принципиально одинаковы — флаг в заголовке просто указывает, присутствует ли альфа-канал или блок полностью непрозрачен.</p><h3>Качество AFRC</h3><p>Качество AFRC впечатляет. В отличие от Metal, здесь можно сделать прямое сравнение с ASTC в реальном времени, поскольку Mali поддерживает сжатие 1:4:</p><p><b>Формат R:</b><br /><br /></p>МетрикаAFRC (1:2)EAC_R Low (1:2)EAC_R Medium (1:2)EAC_R High (1:2)RMSE<b>1,4937</b>2,33992,29221,8636<p></p><p><b>Формат RG:</b><br /><br /></p>МетрикаAFRC (1:2)EAC_RG Low (1:2)EAC_RG Medium (1:2)EAC_RG High (1:2)RMSE<b>2,2079</b>4,22614,15923,3601<p></p><p><b>Формат RGBA:</b><br /><br /></p>МетрикаAFRC (1:2)AFRC (1:4)ASTC 4×4 Low (1:4)ASTC 4×4 Medium (1:4)ASTC 4×4 High (1:4)RMSE<b>0,6679</b><b>3,4184</b>6,29945,96865,3637<p></p><p>Во всех случаях RMSE значительно ниже, то есть AFRC превосходит Spark ASTC при нацеливании на ASTC с кодировщиком реального времени. Тем не менее есть несколько случаев, когда Spark даёт более высокое качество: на очень гладких изображениях сжатие AFRC приводит к заметным паттернам дизеринга, раскрывающим границы блоков. Это особенно заметно при использовании AFRC в качестве текстуры с увеличением, тогда как для сжатия фреймбуфера — основного сценария применения — такие паттерны практически незаметны.</p><figure><img src="https://media.tproger.ru/user-uploads/133946/2026-03-30/7b1601e9-42b2-4416-a0d0-dfda1bf1cafa.webp" alt="Сравнение AFRC с дизерингом и без: AFRC сохраняет детали значительно лучше конкурентов при том же bitrate" /><figcaption>Сравнение AFRC с дизерингом и без: AFRC сохраняет детали значительно лучше конкурентов при том же bitrate</figcaption></figure><h3>Производительность AFRC (Pixel 8)</h3><p>Включение AFRC не вызывает значительных накладных расходов по сравнению с несжатыми загрузками текстур, за исключением некоторых размеров. Результаты на Pixel 8 в МПикс/сек:</p><p></p>Метод409620481024512256Uncompressed4 9613 9513 0632 2902 337AFRC 4 bpc5 5083 7921 7712 3412 318AFRC 2 bpc5 0414 4332 5562 2672 332Spark ASTC Q04 8104 2072 5033 6622 259Spark ASTC Q24 4813 7152 3192 9501 903<p></p><p>Пропускная способность здесь масштабируется с размером текстуры, а не остаётся стабильной. Накладные расходы одинаково влияют на блиты и compute-шейдеры Spark. В отличие от секции Metal, где lossy-блиты явно доминировали над Spark на малых размерах, здесь картина смешанная: Spark вплотную приближается к AFRC или превосходит его. Это показывает, что кодирование текстур в реальном времени вполне конкурентоспособно с аппаратным сжатием. Абсолютные числа здесь значительно ниже, чем на M4 Pro, — это принципиально разные классы устройств.</p><h2>ImgTec PVRIC4: разочарование от Pixel 10</h2><p>ImgTec впервые анонсировала поддержку PVRIC4 ещё в 2018 году для GPU серии Series 6, однако протестировать её удалось только с выходом Pixel 10 на чипе Series D. Первоначальное объявление намекало на то, что, как и Metal Lossy, PVRIC4 поддерживает только 50%-е сжатие, но расширение декларирует более широкий спектр опций:</p><p></p>Формат1 bpc2 bpc3 bpc4 bpcR81 bpp2 bpp3 bpp4 bppRG82 bpp4 bpp6 bpp8 bppRGBA84 bpp8 bpp12 bpp16 bpp<p></p><p>К большому удивлению, качество вывода оказалось одинаковым вне зависимости от BPC. Дальнейшее расследование показало: <b>драйвер игнорирует запрошенный BPC и всегда использует 4 bpc (сжатие 1:2)</b>. Формат блоков PVRIC4 — наиболее сложный из всех трёх вендоров: размер блоков составляет 16×16 пикселей, и, как в Metal Lossy, присутствует один байт метаданных на блок. Сделать реверс-инжиниринг практически не удалось.</p><h3>Качество PVRIC4</h3><p>Качество оказалось разочаровывающим. Для форматов R и RG Spark фактически превосходит PVRIC4 при нацеливании на стандартные форматы блочного сжатия, поддерживаемые этим железом:</p><p><b>Формат R:</b><br /><br /></p>МетрикаPVRIC4 (1:2)BC4 Medium (1:2)BC4 High (1:2)EAC_R Low (1:2)EAC_R Medium (1:2)EAC_R High (1:2)RMSE3,43461,84691,71492,33992,29221,8636<p></p><p><b>Формат RG:</b><br /><br /></p>МетрикаPVRIC4 (1:2)BC5 Medium (1:2)BC5 High (1:2)EAC_RG Low (1:2)EAC_RG Medium (1:2)EAC_RG High (1:2)RMSE5,43923,30993,04424,22614,15923,3601<p></p><p>Для RGBA прямое сравнение невозможно из-за разных коэффициентов сжатия, но качество также значительно хуже других вендоров:</p><p><b>Формат RGBA:</b><br /><br /></p>МетрикаPVRIC4 (1:2)ASTC 4×4 Low (1:4)ASTC 4×4 Medium (1:4)ASTC 4×4 High (1:4)RMSE2,31606,29945,96865,3637<p></p><h3>Производительность PVRIC4 (Pixel 10)</h3><p></p>Метод409620481024512256Uncompressed2 2992 6292 6431 9091 178PVRIC4 4 bpc2 5822 9723 8512 8771 102Spark ASTC Q03 3273 5093 0972 051911Spark ASTC Q23 0022 7592 4981 485634<p></p><p>Кривая пропускной способности на этом устройстве существенно отличается от Pixel 8: пик приходится на размеры 1024–2048, а не монотонно возрастает с размером. На больших размерах пропускная способность Spark фактически выше, чем у несжатых загрузок текстур. Это характерно для устройств, ограниченных пропускной способностью памяти: обычный блит должен прочитать всё входное изображение и записать обратно тот же объём данных, тогда как Spark записывает лишь 1/4 входных данных. Экономия на записи зачастую перекрывает вычислительные затраты кодирования.</p><p><b>Комментарий PowerVR Dev (ImgTec)</b><br />PVRIC4 поддерживает только сжатие 1:2. TFBC (доступен исключительно в чипах Rogue XE) поддерживал соотношения 1:2, 1:4 и 1:3. Дизайн расширения был спорным — многим не нравилась многословность выбора BPC, поэтому было решено экспонировать все BPC в рамках поддерживаемых соотношений и повышать их до поддерживаемого ratio. Это считалось допустимым, поскольку качество не деградирует относительно запроса приложения, а приложение всё равно обязано запрашивать реальный размер изображения, который не гарантирует уменьшения.</p><figure><img src="https://media.tproger.ru/user-uploads/133946/2026-03-30/34deaeec-ec94-4d39-9a82-dd707dcf71a6.webp" alt="Сравнение Spark ASTC с дизерингом и без. Программный Spark показывает хорошие результаты, но AFRC его превосходит" /><figcaption>Сравнение Spark ASTC с дизерингом и без. Программный Spark показывает хорошие результаты, но AFRC его превосходит</figcaption></figure><h2>Spark: программная точка отсчёта</h2><p><a href="https://github.com/NVIDIA/Spark">Spark</a> — библиотека NVIDIA для программного сжатия текстур в реальном времени. Работает на GPU как compute shader, поддерживает форматы BC1–BC7 и ASTC. В тесте выступает базовой точкой сравнения как лучшее доступное программное сжатие без специализированного железа.</p><p>Spark показывает стабильные результаты по форматам, но требует явного вызова compute-прохода — это дополнительный этап в пайплайне. Аппаратное сжатие, напротив, встроено в рендеринг и не требует дополнительного кода. Отдельный плюс Spark — предсказуемое и согласованное поведение на всех устройствах, что важно, если унифицированный вывод критичен для вашего сценария. Ни один из форматов аппаратного сжатия пока не доступен через WebGPU — если это изменится, расширить spark.js для их поддержки будет несложно.</p><h2>Итоги: кто победил</h2><p>ARM AFRC — явный победитель. Это не только превосходит программные реализации вроде Spark, но и обходит все остальные форматы по всем метрикам. Итоговая таблица RMSE по всем протестированным форматам:</p><p></p>ФорматRMSE 1:2RMSE 1:4R8 Metal Lossy1,8579—<b>R8 AFRC</b><b>1,4937</b>—R8 PVRIC43,4346—Spark BC41,7149—RG8 Metal Lossy3,1757—<b>RG8 AFRC</b><b>2,2079</b>—RG8 PVRIC45,4392—Spark BC53,0442—RGBA8 Metal Lossy1,4947—<b>RGBA8 AFRC</b><b>0,6679</b><b>3,4184</b>RGBA8 PVRIC42,3160—Spark BC7—4,2136<p></p><p>Стоит оговориться: результаты PVRIC4 могут не отражать полного потенциала железа — драйвер игнорирует запрошенный коэффициент сжатия и всегда использует 1:2. Возможно, эти результаты удастся пересмотреть после исправления проблемы.</p><p>Аппаратное сжатие — убедительная альтернатива программному. Главная оговорка — оно сейчас ограничено современными топовыми устройствами, которые как раз и располагают наибольшим объёмом памяти и пропускной способностью.</p><p>Даже когда нативное аппаратное сжатие доступно, есть веские причины продолжать использовать Spark. Вывод аппаратного сжатия различается у разных вендоров, и в некоторых случаях — как мы видели с PVRIC4 — качество уступает кодировщику реального времени. Если для вашего сценария критичен единообразный и предсказуемый вывод на всех вендорах, Spark остаётся правильным выбором.</p>]]></content:encoded>
    </item>
    <item>
      <title>Типы языков программирования: от низкоуровневых до высокоуровневых — как выбрать для новичка</title>
      <link>https://tproger.ru/articles/tipy-yazykov-programmirovaniya--ot-nizkourovnevyh-do-vysokourovnevyh---kak-vybrat-dlya-novichka</link>
      <comments>https://tproger.ru/articles/tipy-yazykov-programmirovaniya--ot-nizkourovnevyh-do-vysokourovnevyh---kak-vybrat-dlya-novichka?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Татьяна Жукова]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/tipy-yazykov-programmirovaniya--ot-nizkourovnevyh-do-vysokourovnevyh---kak-vybrat-dlya-novichka</guid>
      <description><![CDATA[<p>Выбираете первый язык программирования? Узнайте о низкоуровневых (C, C++), среднеуровневых (Java, C#) и высокоуровневых (Python, JavaScript) языках: плюсы, минусы и примеры применения. Чек-лист от экспертов поможет новичкам выбрать язык для веб, мобильной разработки или игр.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/tipy-yazykov-programmirovaniya--ot-nizkourovnevyh-do-vysokourovnevyh---kak-vybrat-dlya-novichka">Типы языков программирования: от низкоуровневых до высокоуровневых — как выбрать для новичка</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Инструменты]]></category>
      <category><![CDATA[Производительность]]></category>
      <category><![CDATA[Python]]></category>
      <category><![CDATA[Ruby]]></category>
      <category><![CDATA[PHP]]></category>
      <category><![CDATA[Windows]]></category>
      <category><![CDATA[Работа]]></category>
      <category><![CDATA[.NET]]></category>
      <category><![CDATA[GitHub]]></category>
      <category><![CDATA[Боты]]></category>
      <category><![CDATA[TypeScript]]></category>
      <category><![CDATA[Xamarin]]></category>
      <category><![CDATA[Фронтенд]]></category>
      <category><![CDATA[QA]]></category>
      <category><![CDATA[Бэкенд]]></category>
      <category><![CDATA[Аналитика]]></category>
      <category><![CDATA[Data Science]]></category>
      <category><![CDATA[Приложение]]></category>
      <category><![CDATA[Библиотеки]]></category>
      <category><![CDATA[Сервер]]></category>
      <category><![CDATA[Dart]]></category>
      <category><![CDATA[Разработка]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Tue, 28 Oct 2025 14:00:03 GMT</pubDate>
      <content:encoded><![CDATA[<p>Для погружения в программирование нужно всего 3 вещи:</p><ul><li>Решить, с какого языка/технологии вы хотите начать.</li><li>Решить, на каком ресурсе вы хотите обучаться.</li><li>Выделить время на само программирование.</li></ul><p>Звучит просто, однако у вас уйдёт много времени на исследования, чтобы решить, что вам подходит и на каком ресурсе обучаться.</p><p>Некоторые люди начинают с относительно низкоуровневого программирования на C и C++. Другие выбирают более традиционный путь, изучая Java или C#. Есть и те, кто начинает с высокоуровневых или скриптовых языков вроде Python, Ruby или JavaScript.</p><p>Мы классифицируем языки по уровню абстракции. Для новичков: низкоуровневые — как ручная сборка машины (контроль, но сложный); среднеуровневые — как готовый конструктор с инструкцией (сохраняем баланс); высокоуровневые — как приложение на смартфоне (быстро, но меньше контроля). Ниже разберём плюсы и минусы и поможем сделать правильный выбор.</p><h2>Низкоуровневые языки: близко к «железу»</h2><p>Это языки, где вы напрямую работаете с памятью компьютера. Нет автоматической уборки ненужных данных, <b>всё под вашим контролем</b>. Подходят для системного ПО, игр или устройств (например, микроконтроллеров).</p><p>Примеры: C (для ОС вроде Linux), C++ (для игр на движке вроде Unreal Engine), Assembler (для оптимизации критических частей кода).</p><p><b>Плюсы:</b></p><ul><li>Полный контроль: вы решаете, как использовать ресурсы. Так, в C++ можно вручную выделять память для массивов, избегая ненужных копий данных.</li><li>Высокая скорость: прямой доступ к памяти позволяет писать код, который работает быстрее (это важно для работы с играми или серверами).</li><li>Основы основ: такие языки учат, как компьютер работает изнутри, чтобы в будущем ценить удобства других языков. Например, вы узнаёте, почему «утечка памяти» — это проблема.</li><li>Эффективность: низкоуровневые языки мотивируют думать об оптимизации заранее, снижая расход батареи или CPU.</li><li>Компактность: минимальная библиотека, приложения получаются лёгкими (идеально для embedded-систем, как в IoT-устройствах).</li></ul><p>Минусы:</p><ul><li>Всё-таки это сложно: рутинные задачи (например, чтение файла) требуют больше кода и внимания к деталям, рискуя ошибками вроде переполненного буфера.</li><li>Ручное управление памятью: можно легко «забыть» освободить память, вызвав утечки или краши. Например, в C нужно использовать malloc/free, иначе программа съест всю RAM.</li><li>Много копипасты: придётся писать шаблонный код, и делать это часто.</li><li>Платформо-зависимость: код для Windows может не работать на Linux без правок.</li></ul><h2>Среднеуровневые языки: баланс контроля и удобства</h2><p>Эти языки предлагают готовые инструменты, упрощающие работу, но требуют строгой проверки типов данных. Для их запуска нужна специальная программа (среда выполнения). Они идеальны для приложений, серверов и игр.</p><p>Примеры: Java (для Android-приложений), C# (для Unity-игр или .NET-серверов).</p><p>Плюсы:</p><ul><li>Автоматическая память: память очищается автоматически благодаря сборщику мусора, что избавляет от ручной работы и снижает вероятность ошибок, вроде утечек памяти в больших проектах. При этом можно получить доступ к низкоуровневым функциям для особых задач, например, через специальные инструменты в Java.</li><li>Богатые библиотеки: готовые инструменты для сетей, GUI или баз данных. Пример: Java’s Spring для веб-серверов.</li><li>Кроссплатформенность: компиляция в байт-код (JVM для Java) позволяет запускать код везде. Например, пишешь на Windows, запускаешь на Linux.</li><li>Безопасность: язык проверяет типы данных перед запуском программы, помогая заранее найти ошибки. Среда выполнения защищает от опасных сбоев, например, от переполненной памяти.</li><li>Масштабируемость: встроенные инструменты для параллельных вычислений позволяют легко создавать программы, которые одновременно выполняют много задач (серверы для тысяч пользователей и т.п.).</li></ul><p>Минусы:</p><ul><li>Дополнительная нагрузка от рантайма: Среда выполнения и автоматическая очистка памяти создают дополнительную нагрузку. Сборщик мусора может ненадолго останавливать программу, что заметно в играх или при обработке видео.</li><li>Меньше контроля: абстракции скрывают детали памяти, усложняя оптимизацию (например, в Java сложно избежать боксинга примитивов).</li><li>Повторяющийся код, которого много: приходится писать повторяющийся код и тренировать свою усидчивость, например, для доступа к данным объекта. Кстати, инструменты вроде Lombok могут упростить эту задачу.</li><li>Зависимость от среды: для запуска программ нужна специальная среда (например, JVM для Java), что влияет на размер программ и замедляет их старт.</li><li>Сложность интеграции: подключение кода на других языках, например, на C, требует писать специальные обёртки, что усложняет работу и снижает скорость.</li></ul><h2>Высокоуровневые языки: удобство и скорость разработки</h2><p>Эти языки скрывают технические детали, позволяя сосредоточиться на создании <b>логики программы</b>. Подходят для веба, data science или скриптов.</p><p>Примеры: Python (для ML), Ruby (для веб-разработки), JavaScript (для фронтенда).</p><p>Плюсы:</p><ul><li>Простота: сложные задачи решаются в пару строк. Так, в JS async/await упрощает API-запросы.</li><li>Быстрая разработка: динамическая типизация позволяет быстро писать и тестировать код без необходимости его компиляции.</li><li>Богатые экосистемы: есть библиотеки для всего (к примеру, NumPy для данных в Python).</li><li>Гибкость: легко менять код, идеально для прототипов или стартапов.</li></ul><p>Минусы:</p><ul><li>Низкая производительность: абстракции добавляют нагрузки. Например, циклы в Python медленнее, чем в C.</li><li>Ошибки на рантайме: из-за слабой типизации ошибки выявляются только при запуске программы, что усложняет отладку.</li><li>Риск «спагетти-кода»: лёгкость изменений может привести к хаосу без дисциплины.</li><li>Скрытые проблемы: абстракции маскируют баги.</li><li>Зависимость от интерпретатора: программы требуют установленного интерпретатора, что замедляет их запуск и добавляет зависимость от дополнительного ПО.</li></ul><h2>Чек-лист: как выбрать язык программирования для новичка</h2><p>Этот чек-лист основан на советах экспертов, чтобы помочь новичкам выбрать первый язык программирования. Каждый пункт включает конкретные рекомендации.</p><ol><li><b>Определите, что вас вдохновляет, и выберите сферу.</b> Подумайте, что вы хотите создавать: сайты, игры, мобильные приложения или серверы. Разные сферы требуют разных языков. Например, для веб-разработки подойдут JavaScript или Python, для мобильных приложений — Java, Kotlin, Swift или Dart, а для игр — C# или C++. Составьте список идей (например, сайт-визитка, игра, аналитика данных) и найдите, какие языки для них используют. <br />— Аня Жаркова, руководитель мобильной разработки в USETECH</li><li><b>Ищите язык с широким применением.</b> Выбирайте языки, которые используются в разных областях, чтобы легче переключаться между задачами. Например, Kotlin подходит для мобильной разработки, веба и серверов, а C# — для десктоп-приложений, игр (Unity) и бэкенда. Это даёт гибкость и упрощает изучение новых языков в будущем.<br />— Аня Жаркова, руководитель мобильной разработки в USETECH</li><li><b>Проверьте спрос на рынке труда. </b>Если цель — смена профессии, изучите вакансии на HH.ru или LinkedIn. Введите «junior Python», «junior Java» и сравните, где больше предложений и какие требования. Избегайте языков с низким спросом, если хотите быстро найти работу. Например, Java, Kotlin, Python и JavaScript популярны для найма.<br />— Владислав Масунов, Head of Development</li><li><b>Опробуйте языки на практике.</b> Напишите простые программы (например, "Hello World" или калькулятор) на нескольких языках, чтобы понять, какой синтаксис вам ближе. Используйте онлайн-редакторы вроде Replit или CodePen. Например, попробуйте TypeScript для веба (он поддерживает типизацию и разные подходы к программированию) или C++ для понимания работы с памятью. Это поможет почувствовать, к чему лежит душа.<br />— Рома Троицкий, фронтенд-инженер в Сбер B2C, член ПК HolyJS &amp; MoscowCSS; Евгений Антонов, ИТ-консультант, автор тг-канала <a href="https://t.me/general_it_talks">@general_it_talks</a></li><li><b>Выберите язык с хорошей документацией и сообществом.</b> Убедитесь, что у языка много обучающих материалов и активное сообщество. Например, TypeScript имеет богатую документацию и поддержку, что упрощает старт. Проверьте ресурсы вроде LearnPython.org, freeCodeCamp для JavaScript или Telegram-чаты для C++. Это поможет быстрее решать вопросы.<br />— Рома Троицкий, фронтенд-инженер в Сбер B2C, член ПК HolyJS &amp; MoscowCSS</li><li><b>Учитывайте сложность и карьерные цели.</b> Для небольших проектов или быстрого старта берите Python или PHP — они проще и подходят для веб-разработки или скриптов. Для сложных задач с высокой нагрузкой (например, серверы или оптимизация) попробуйте C++ или Go. Если цель — работа в крупных компаниях, Java и Kotlin востребованы и часто используются с ИИ-инструментами. Выбирайте, исходя из сложности и ваших амбиций.<br />— Евгений Антонов, ИТ-консультант, автор тг-канала <a href="https://t.me/general_it_talks">@general_it_talks</a></li><li><b>Смотрите на универсальность и переход к другим языкам. </b>Выбирайте язык, который учит основам программирования и упрощает переход к другим. Например, изучение C# может помочь освоить Java, а затем — Android-разработку. TypeScript учит объектно-ориентированному и функциональному программированию, что полезно для разных задач. Это создаёт базу для дальнейшего роста.<br />— Аня Жаркова, руководитель мобильной разработки в USETECH; Рома Троицкий, фронтенд-инженер в Сбер B2C, член ПК HolyJS &amp; MoscowCSS</li><li><b>Не гонитесь за гилти плежа.</b> Избегайте языков, которые изучают «для удовольствия» без практического применения. Выбирайте те, которые можно применить в реальных проектах или которые востребованы в индустрии. Например, вместо нишевых языков берите Python, Java или Kotlin: они имеют чёткие сценарии использования.<br />— Аня Жаркова, руководитель мобильной разработки в USETECH</li><li><b>Практикуйтесь с ИИ-инструментами.</b> Если хотите работать в крупных компаниях, освойте язык, который хорошо сочетается с ИИ-инструментами (например, Go или Java). Практикуйтесь с ИИ-агентами по типу GitHub Copilot для автоматизации задач — это ценится работодателями.<br />— Евгений Антонов, ИТ-консультант, автор тг-канала <a href="https://t.me/general_it_talks">@general_it_talks</a></li><li><b>Составьте план развития.</b> Создайте roadmap: определите, какие проекты хотите делать через 3-6 месяцев (например, мобильное приложение или веб-сервис), и подберите язык под эти цели. Если выбрали C#, начните с десктоп-приложений, затем попробуйте Xamarin для кроссплатформенной разработки. Постепенно добавляйте новые языки, опираясь на первый.<br />— Аня Жаркова, руководитель мобильной разработки в USETECH</li></ol><p>Если не определились, предлагаем пройти квиз и расставить всё по полочкам:</p>]]></content:encoded>
    </item>
    <item>
      <title>Как освоить ассемблер для разработчиков Swift: первые шаги</title>
      <link>https://tproger.ru/articles/kak-osvoit-assembler-dlya-razrabotchikov-swift--pervye-wagi</link>
      <comments>https://tproger.ru/articles/kak-osvoit-assembler-dlya-razrabotchikov-swift--pervye-wagi?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Татьяна Жукова]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/kak-osvoit-assembler-dlya-razrabotchikov-swift--pervye-wagi</guid>
      <description><![CDATA[<p>Освойте основы ассемблера для Swift: узнайте, как работают регистры, настройте Xcode для компиляции .s-файлов и напишите первую программу на ассемблере. Практическое руководство для iOS-разработчиков.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/kak-osvoit-assembler-dlya-razrabotchikov-swift--pervye-wagi">Как освоить ассемблер для разработчиков Swift: первые шаги</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Мобильная разработка]]></category>
      <category><![CDATA[Производительность]]></category>
      <category><![CDATA[iOS]]></category>
      <category><![CDATA[Swift]]></category>
      <category><![CDATA[macOS]]></category>
      <category><![CDATA[Objective-C]]></category>
      <category><![CDATA[Xcode]]></category>
      <category><![CDATA[Разработка]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 17 Sep 2025 10:00:04 GMT</pubDate>
      <content:encoded><![CDATA[<p><i>Статья <a href="https://arturgruchala.com/assembler-for-swift-developers/">переведена</a> с оригинала польского iOS-разработчика Артура Грачаты, который делится практическими советами по низкоуровневому программированию для улучшения навыков Swift-разработчиков.</i></p><h2>Зачем разработчику Swift изучать ассемблер</h2><p>Ассемблер — это не просто низкоуровневое программирование, а способ понять, как код на Swift или Objective-C преобразуется в инструкции процессора. Он помогает разобраться в движении данных, оптимизировать производительность и отлаживать сложные ошибки. Изучение ассемблера не требует писать целые приложения на нем, но дает уверенность и глубокое понимание работы системы. К тому же, умение работать на уровне процессора добавляет разработчику уникальности и открывает новые карьерные перспективы.</p><h2>Регистры: основа работы процессора</h2><p>Регистры — это быстрые ячейки памяти процессора, своего рода «рабочий стол» для данных. На архитектуре Apple Silicon (ARM64) они выполняют следующие роли:</p><ul><li>x0–x7: передача аргументов и возвращаемых значений.</li><li>x8: регистр косвенных результатов (используется редко).</li><li>x9–x15: временные регистры для промежуточных вычислений.</li><li>x16–x17: для системных вызовов (например, указание номера вызова).</li><li>x18: регистр платформы, сохраняемый между вызовами.</li><li>x19–x28: регистры, которые нужно сохранять при вызове функций.</li><li>sp (x31): указатель стека, хранящий локальные данные.</li><li>pc: счетчик программы, указывающий на следующую инструкцию.</li></ul><p>При выполнении системного вызова (например, для вывода текста в консоль) аргументы загружаются в регистры x0, x1 и далее, номер вызова — в x16, а команда svc 0 передает управление ядру системы.</p><h2>Настройка Xcode для работы с ассемблером</h2><p>Чтобы начать писать код на ассемблере, нужно настроить проект в Xcode:</p><ol><li>Откройте Xcode и создайте новый проект: File → New → Project….</li><li>Выберите Command Line Tool в разделе macOS, затем нажмите Next.</li><li>Назовите проект, например, HelloAsm, выберите язык C (его позже заменим) и нажмите Create.</li><li>В Project Navigator удалите файл main.c.</li><li>Добавьте новый пустой файл: File → New → File… → Other → Empty, назовите его main.s и нажмите Create.</li><li>В настройках проекта откройте Build Phases и убедитесь, что main.s добавлен в Compile Sources.</li></ol><p>Теперь Xcode будет компилировать файл main.s как точку входа для программы на ассемблере.</p><h2>Первая программа: «Hello, Assembly!»</h2><p>Рассмотрим минимальную программу, которая выводит «Hello, Assembly!» в терминал. Вот ее структура с пояснениями:</p><ul><li>.section __TEXT,__text &amp; .global _main: указывает, что это исполняемый код, а _main — точка входа.</li><li>mov x0, 1: задает 1 (stdout) как первый аргумент системного вызова.</li><li>adrp/add: загружает адрес строки msg в регистр x1 (с учетом разбиения на страницы).</li><li>mov x2, #0x11: задает длину сообщения (включая символ новой строки и нулевой терминатор).</li><li>mov x16, #0x4: указывает номер системного вызова для записи в macOS.</li><li>svc 0: выполняет системный вызов, передавая управление ядру.</li><li>Повторение для вызова выхода.</li></ul><p>Примечание: в коде используются шестнадцатеричные числа. Знание преобразования между десятичными, двоичными и шестнадцатеричными числами пригодится.</p><h2>Сборка и запуск</h2><p>Соберите и запустите проект в Xcode. При успешной компиляции в терминале появится:</p><p>Hello, Assembly!</p><p>Если вывод не появился, проверьте:</p><ul><li>Файл main.s добавлен в Compile Sources.</li><li>Нет лишних C-файлов, которые могут переопределить main.</li><li>Архитектура проекта соответствует вашему оборудованию (Intel или Apple Silicon).</li></ul><h2>Что дальше</h2><p>Написав первую программу на ассемблере, вы сделали шаг к пониманию работы процессора. Продолжайте изучать регистры — это упростит отладку и поможет лучше понимать, где находятся данные в вашем коде.</p>]]></content:encoded>
    </item>
    <item>
      <title>Веб-платформа для изучения GPU-программирования</title>
      <link>https://tproger.ru/articles/veb-platforma-dlya-izucheniya-gpu-programmirovaniya</link>
      <comments>https://tproger.ru/articles/veb-platforma-dlya-izucheniya-gpu-programmirovaniya?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Александр]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/veb-platforma-dlya-izucheniya-gpu-programmirovaniya</guid>
      <description><![CDATA[<p>Платформа для изучения GPU-программирования через решение задач на GLSL и HLSL. Более 250 интерактивных заданий, сгруппированных по темам: от базовых функций и математики до освещения и постобработки.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/veb-platforma-dlya-izucheniya-gpu-programmirovaniya">Веб-платформа для изучения GPU-программирования</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Обучение программированию]]></category>
      <category><![CDATA[Обучающие курсы]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Sun, 07 Sep 2025 12:00:03 GMT</pubDate>
      <content:encoded><![CDATA[<p>Всем привет! Я работаю рендер-программистом в крупной игровой компании. До этого много лет занимался разработкой веб-приложений на аутсорсе как на фронтенд, так и на серверной части.</p><p>В какой-то момент появилось желание, используя накопленный опыт, создать собственный пет-проект. Так появился <a href="https://shader-learning.com/">Shader-Learning.com</a>, который я уже более двух лет разрабатываю и совершенствую в свободное время. Это платформа, созданная для того, чтобы помочь людям изучать и практиковать GPU-программирование на <b>GLSL </b>и <b>HLSL </b>прямо в браузере.</p><p>На платформе уже <b>более 250 интерактивных заданий</b>, которые пошагово помогают вам разобраться в ключевых концепциях программирования графики.</p><p>Все задания сгруппированы по модулям, чтобы можно было сосредоточиться на темах, которые наиболее интересны:</p><ul><li>роль фрагментного и вершинного шейдеров в графическом конвейере и изучение встроенных функций GLSL/HLSL;</li><li>базовая математика и геометрия, лежащие в основе компьютерной графики – от векторов и матриц до пересечений примитивов и систем координат;</li><li>алгоритмы обработки 2D-изображений с помощью фрагментных шейдеров;</li><li>реализация освещения и теней;</li><li>рендеринг травы и воды в реальном времени;</li><li>использование функций шума для процедурной генерации визуальных эффектов;</li><li>Продвинутые техники: билборды, Soft Particles, MRT, отложенный рендеринг, HDR, туман и другое;</li><li>SDF.</li></ul><h3>Примеры заданий</h3><p>Первое задание посвящено тому, что такое шейдерная программа и как её использовать для закрашивания пикселей на экране:</p><figure><img src="https://media.tproger.ru/user-uploads/117591/2025-09-04/d976d1f0-4944-479c-a7b1-e98eac913b47.png" alt="" /><figcaption>необходима закрасить экран в нужный цвет</figcaption></figure><p>Задача из модуля по математике: с помощью скалярного произведения необходимо разделить экран на две части.</p><figure><img src="https://media.tproger.ru/user-uploads/117591/2025-09-04/04af9d8a-5757-455d-a3ae-14d01bfb9927.png" alt="" /><figcaption>скалярное произведение разделить экран</figcaption></figure><p>Иногда затрагиваю низкоуровневые моменты работы GPU:</p><figure><img src="https://media.tproger.ru/user-uploads/117591/2025-09-04/06d78f9d-cd72-492f-9c82-fa3ddc3d1c3f.png" alt="" /><figcaption>переписать код без условного ветвления</figcaption></figure><p>Есть задачи, в которых я предлагаю пользователю работать не с примитивами, а уже с полноценной сценой:</p><figure><img src="https://media.tproger.ru/user-uploads/117591/2025-09-04/667eda95-61f5-47a0-9d34-dc08a864c38e.png" alt="" /><figcaption>добавить туман на сцену</figcaption></figure><p>Каждую задачу вы решаете локально в браузере, а когда считаете, что она выполнена правильно, отправляете её на проверку:</p><figure><img src="https://media.tproger.ru/user-uploads/117591/2025-09-04/53bd58bc-3b7d-4571-80b3-eede6977fa84.png" alt="" /><figcaption>решение принято</figcaption></figure><p>Есть функция «Diff», которая отображает разницу между ожидаемым результатом и результатом пользователя. Она помогает выявить разницу, которую сложно увидеть глазом:</p><figure><img src="https://media.tproger.ru/user-uploads/117591/2025-09-04/68726e69-fbbf-4a9a-aaeb-3e57c3ec9c8b.png" alt="" /><figcaption>diff</figcaption></figure><p>Если у вас возникнут вопросы или трудности при решении заданий, вы всегда можете обратиться за разъяснением и помощью в <a href="https://discord.com/channels/1193522220249653350/1193525446395056148">Discord</a>. Я стараюсь активно отвечать и помогать.</p><h3>Пользовательские задачи</h3><figure><img src="https://media.tproger.ru/user-uploads/117591/2025-09-04/b9df0d79-8da2-4077-a760-cbb1504fc765.png" alt="" /><figcaption>редактор пользовательской задачи</figcaption></figure><p>У пользователей есть возможность создавать и делиться собственными задачами. Редактор задач позволяет построить графическую сцену с полным контролем над её компонентами:</p><p><b>Настройка сцены:</b></p><ul><li>настроить камеру и её анимацию;</li><li>добавить объекты на сцену и указать их геометрию;</li><li>загрузить текстуры и назначить их материалам.</li></ul><p><b>Редактирование шейдеров:</b></p><ul><li>можно изменять вершинный и/или фрагментный шейдеры;</li><li>добавить шейдер постобработки для экранных эффектов.</li></ul><p><b>Содержание задачи:</b></p><ul><li>указать описание условия вашей задачи;</li><li>указать теоретическую или вводную информацию, которая может быть полезна для решения задачи.</li></ul><p><b>Настройки проверки выполнения</b></p><ul><li>какие файлы может редактировать пользователь (фрагментный, вертексный или шейдер постобработки);</li><li>какие кадры должны сравниваться, если задача анимированная;</li><li>правила линтера (например, запретить использование if).</li></ul><p><b>Публикация и распространение</b></p><p>После создания и публикации задача становится доступной мгновенно. Вы можете сразу делиться ссылкой с другими пользователями.</p><p><b>Статистика задачи. </b>Для каждой опубликованной задачи вы можете отслеживать:</p><ul><li>количество просмотров;</li><li>количество успешных решений;</li><li>лайки и дизлайки;</li><li>текстовые отзывы от пользователей.</li></ul><h3>Управление задачами</h3><p>В любой момент вы можете:</p><ul><li>редактировать задачу;</li><li>скрыть её из общего доступа;</li><li>повторно опубликовать, когда будете готовы.</li></ul><h3>Стек Технологий</h3><p>Node.js, Express.js, MySQL, Amazon S3, Angular, NgXs, RxJS, Typescript, SCSS, WebGL, Three.js</p><h3>Заключение</h3><p>Несмотря на то, что на платформе есть платный контент, я надеюсь, что мой пост не будет воспринят негативно.   Вы можете совершенно бесплатно и без рекламы решать задачи из модулей<a href="https://shader-learning.com/module-view/built-in-functions"> Built-in Functions</a> и <a href="https://shader-learning.com/module-view/advanced-rendering">Advanced Rendering</a>, а также решать <a href="https://shader-learning.com/community/tasks">пользовательские задания</a> и создавать свои.</p><p>Буду рад любым вашим идеям и предложениям по развитию платформы!</p><p>Если интересно, присоединяйтесь к <a href="https://discord.com/channels/1193522220249653350/1193525446395056148">Discord</a>, чтобы не пропустить новые уроки и обновления.</p><p>Спасибо за внимание!</p>]]></content:encoded>
    </item>
    <item>
      <title>Квантовые вычисления для разработчиков: когда нам придется учить новую математику?</title>
      <link>https://tproger.ru/articles/kvantovye-vychisleniya-dlya-razrabotchikov--kogda-nam-pridetsya-uchit-novuyu-matematiku-</link>
      <comments>https://tproger.ru/articles/kvantovye-vychisleniya-dlya-razrabotchikov--kogda-nam-pridetsya-uchit-novuyu-matematiku-?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Вадим Егорцев]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/kvantovye-vychisleniya-dlya-razrabotchikov--kogda-nam-pridetsya-uchit-novuyu-matematiku-</guid>
      <description><![CDATA[<p>Квантовые вычисления для разработчиков. Показываем, как квантовые вычисления помогут в программировании. Рассматриваем основные подходы и прогнозы на будущее ✔ Tproger</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/kvantovye-vychisleniya-dlya-razrabotchikov--kogda-nam-pridetsya-uchit-novuyu-matematiku-">Квантовые вычисления для разработчиков: когда нам придется учить новую математику?</a>»</p>]]></description>
      <category><![CDATA[API]]></category>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Операционные системы]]></category>
      <category><![CDATA[Языки программирования]]></category>
      <category><![CDATA[Microsoft]]></category>
      <category><![CDATA[Квантовые вычисления]]></category>
      <category><![CDATA[IBM]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Fri, 21 Mar 2025 10:00:02 GMT</pubDate>
      <content:encoded><![CDATA[<p>Авторы научных работ по квантовой механике выдвигают противоречивые теории о физике частиц. Каждый ученый приводит доказательства, их ход мыслей кажется логичными.</p><blockquote>«Я думаю, что могу смело утверждать: квантовую механику не понимает никто».</blockquote><p>По этой причине многие идеи оставим без внимания — попытка их объяснения растянется на десяток страниц. Также не будет формул и страшных математических моделей.</p><p>Рассмотрим лишь то, что приблизит к ответу на вопрос: как квантовые вычисления повлияют на программирование в будущем.</p><h2>Определения и квантовая путаница</h2><h3>Кубиты</h3><p>Квантовые вычисления строятся на основе <b>кубитов</b>. Они существуют как физические объекты. Законы квантовой механики проявляются на фотонах, электронах, ионах, атомах.</p><figure><img src="https://media.tproger.ru/user-uploads/105601/2025-03-17/751e13eb-2cfa-42c8-8e68-d05bc09388ff.jpg" alt="" /><figcaption>Бит содержит только одно значение, кубит принимает все значения одновременно</figcaption></figure><p>Самое интересное — если обычный бит принимает значение 0 или 1, то кубит находится в <b>суперпозиции</b>. Его состояние описывают диапазоном от 0 до 1, и оно непрерывно меняется.</p><p>Суперпозицию можно рассмотреть на примере монетки. На столе она лежит орлом вверх (0), либо решкой (1). Теперь представьте, что монету подбросили, — она вращается в воздухе и находится как бы в обоих состояниях одновременно.</p><h3>Квантовая запутанность</h3><p>В квантовой механике что не явление, то необъяснимое событие. Одно из таких — <b>запутанность</b>. Два отдельных кубита оказываются «связаны», игнорируя расстояние.</p><p>Например, если измеряете один кубит, то автоматически узнаете состояние другого. Даже если второй находится на Марсе.</p><h3>Квантовый брутфорс</h3><p>Битами классических компьютеров управляют логические операторы — И, ИЛИ, НЕ.</p><figure><img src="https://media.tproger.ru/user-uploads/105601/2025-03-17/409f672b-7158-47f4-b759-b82ebaeb8a58.jpg" alt="" /><figcaption>Оператор НЕ инвертирует значение классического бита. При работе с кубитом используются квантовые преобразования, которые являются обратимыми</figcaption></figure><p>Состоянием кубитов управляют <b>гейты</b>. Они «переставляют» квантовые вероятности, чтобы получить диапазон с правильным ответом.</p><p>Представьте замок и 40 ключей. Только один из них подходит. Классический компьютер попробует каждый ключ по очереди. Квантовый образец одновременно «проверит» все варианты, затем с помощью гейтов сузит поиск до верного.</p><p>Суть в том, что квантовые методы вычисляют ответ всего за √N попыток вместо N, как в классическом переборе.</p><p>Последнее испытание гибкости вашей фантазии: <b>гейты выводят и удерживают кубиты в суперпозиции</b>. Это невероятно сложная задача.</p><p>Представьте идеально сбалансированную иголку. Ваша задача, как ученого, — установить иглу на самый кончик и удерживать в равновесии как можно дольше.</p><h2>Он — компьютер, но квантовый</h2><p>Есть мнение, что <b>квантовый компьютер (КК)</b> вовсе не компьютер, потому что выдает не точный, а приблизительный ответ.</p><p>Но он все равно компьютер! У него есть процессор, память, он следует алгоритмам. КК принимает входные данные и выдает результат, который можно обработать.</p><p>Справедливое замечание: без классического компьютера квантовые вычисления — бесполезны.</p><figure><img src="https://media.tproger.ru/user-uploads/105601/2025-03-17/8a1b0a82-38b7-45ae-b430-c965902b39d8.jpg" alt="" /><figcaption>Квантовый компьютер IBM</figcaption></figure><p>КК отсекает решения, невозможные из-за противоречивости, и обозначает предел с ответом. Искать это точное решение и выбирать из нескольких выданных вариантов — задача обычного ПК.</p><p>КК можно сравнить с аналоговым компьютером. В обоих устройствах происходит не вычисление, а измерение результата.</p><h2>Как квантовые вычисления повлияют на программирование</h2><p>Криптог<b>р</b>афия — одна из первых областей, где развитие индустрии квантовых вычислений обещает серьезные изменения. Современные методы шифрования уязвимы при применении квантовых алгоритмов.</p><p>С появлением достаточно мощного КК использование RSA и AES будет небезопасным. Поэтому IT-компании уже разрабатывают <a href="https://tproger.ru/articles/skolko-vremeni-u-nas-ostalos-do-ugrozy-kriptografii-">квантовую криптографию</a>, чтобы защитить данные до появления угрозы.</p><p>В будущем интеграция квантовых технологий решит проблемы <b>оптимизации</b>. Компьютеры будут определять наилучшее решение среди огромного числа вариантов. Актуально для маршрутизации и планирования производства.</p><p><b>Машинное обучение</b> тоже выигрывает от развития квантовых вычислений. Скорость обучения нейросетей увеличится в разы.</p><h3>Классические алгоритмы vs квантовые алгоритмы</h3><p>Классические алгоритмы работают последовательно, а квантовые сокращают количество шагов благодаря параллельной обработке через суперпозиции кубитов.</p><p>Например, алгоритм Шора. Он раскладывает числа на множители, но работает только на квантовом компьютере.</p><p>На классическом ПК методы нахождения множителей начинают работать медленнее. Скорость замедления растет с величиной числа. Классические методы требуют так много вычислений, что время выполнения растет экспоненциально (как снежный ком):</p><ul><li>Для числа размером 2 цифры — очень легко.</li><li>Для числа размером 10 цифр — сложнее, но реально.</li><li>Для числа размером в 300 цифр (как в RSA-криптографии) — может занять тысячи лет.</li></ul><p>С развитием индустрии квантовых технологий число, на разложение которого ушли бы тысячи лет, будет разобрано за несколько часов или дней.</p><p>Другой пример — алгоритм Гровера. Он ускоряет поиск в неструктурированных базах данных.</p><p>В списке из 1000 элементов обычный компьютер проверит до 1000 записей. Алгоритм Гровера проверит всего √1000 ≈ 31 запись.</p><h2>Нужны ли новые языки программирования?</h2><p>Для квантовых измерений только традиционных языков программирования недостаточно.</p><h4>Qiskit</h4><p>Разработчик: IBM</p><p>Популярный Python-фреймворк для программирования квантовых алгоритмов. На нем можно написать программу и выполнить ее на симуляторе. Есть возможность запускать алгоритмы на реальных квантовых компьютерах IBM через облако.</p><h4>Cirq</h4><p>Разработчик: Google</p><p>Библиотека на Python для создания и тестирования технологий квантового вычисления. Cirq чаще используют для работы с алгоритмами Гровера, Шора и др. Программу можно протестировать на симуляторе.</p><h4>Q#</h4><p>Разработчик: Microsoft</p><p>Q# — это часть <a href="https://learn.microsoft.com/en-us/azure/quantum/install-overview-qdk">Microsoft Quantum Development Kit</a> (QDK). Он содержит инструменты для симуляции, разработки программ и интеграции квантовых вычислений с кодом на C# или Python.</p><h2>Какая математика потребуется разработчикам?</h2><p>Разработчик должен уметь работать с <b>матрицами </b>и <b>векторами</b>, потому что с их помощью описывают состояния кубитов и операции над ними.</p><figure><img src="https://media.tproger.ru/user-uploads/105601/2025-03-17/35bc3bfc-41ac-439e-a801-f9a2b9c60db7.jpg" alt="" /><figcaption>Фрагмент задачи по квантовой механике</figcaption></figure><p>Векторы показывают, где кубит «находится» в пространстве, а матрицы управляют его изменениями.</p><p>Все квантовые операции — это просто умножение вектора состояния на матрицы. Но результат нельзя узнать до момента измерения, поэтому вероятность каждого расчета задается квадратичными модулями коэффициентов.</p><p>Квантовые вычисления невозможно представить без <b>комплексных чисел</b>. Они учитывают направление фазы, необходимое для описания интерференции (суперпозиции).</p><p>В будущем программист должен понимать, как измерения кубитов приводят к результатам с заданной вероятностью, и уметь моделировать суперпозицию.</p><h2>Когда начинать изучать квантовые вычисления?</h2><p>Развитие квантовых технологий находится на этапе перехода от исследовательской стадии к практике в реальных приложениях. До массового внедрения далеко, десятки лет.</p><p>Уходить сегодня из фронтенда в разработку квантовых программ — это как создавать ИИ в 80-х годах.</p><p>Основное ограничение — количество активных кубитов и стабильность вычислений. Процессор IBM Condor с 1121 кубитом побил рекорд по мощности, но поддержание кубитов в суперпозиции и подавление квантового шума остаются нерешенными проблемами.</p><p>Сегодня использование квантовых вычислений сводится к узким задачам: симуляции материалов, исследованию химических соединений.</p><h3>Какие компании уже инвестируют в квантовые вычисления?</h3><p><b>IBM </b>— лидер в разработке квантовых технологий. Их платформа IBM Quantum Experience предоставляет доступ к настоящим квантовым компьютерам через облако.</p><p>В 2016 году компания запустила первый публичный квантовый процессор и продолжает увеличивать масштаб вычислений, предлагая обучение и инструменты для разработчиков Qiskit.</p><figure><img src="https://media.tproger.ru/user-uploads/105601/2025-03-17/31ed3a87-6a14-4b2b-9abb-0f8ebde7fac2.jpg" alt="" /><figcaption>Квантовый процессор Google на 54 кубита</figcaption></figure><p><b>Google</b> тоже добилась успеха, заявив о квантовой супрематии — решении задачи, недостижимой для классических компьютеров, с помощью своего процессора Sycamore. Google работает над оптимизацией алгоритмов и предоставляет инструменты для программирования на платформе Cirq.</p><p><b>Microsoft </b>раньше других взялась за развитие языка программирования для квантовых вычислений — Q#. Вместе с этим доступен Quantum Development Kit, который моделирует квантовые программы и тестирует на симуляторах.</p><p><b>Amazon </b>тоже не осталась в стороне. Компания предлагает облачный сервис AWS Braket, интегрирующий работу с квантовыми симуляторами.</p><p>При появлении первых ПК компании разрабатывали собственные операционные системы, процессоры и платформы. Сегодня квантовые технологии находятся на той же стадии — это начало глобальной гонки за квантовую мощь.</p><p><b>Через 5 лет</b> квантовые компьютеры, вероятно, достигнут промежуточной стадии применения. Усилия будут направлены на коррекцию ошибок.</p><p><b>Через 10 лет</b> можно ожидать стабильных образцов с тысячами кубитов. Это станет отправной точкой для коммерческой квантовой индустрии.</p><p><b>Спустя 20 лет</b> квантовые компьютеры, вероятно, станут частью бизнеса. Их будут применять разработчики, работающие с криптографией, ИИ, аналитикой. К этому времени квантовые вычисления станут массовым явлением.</p><p>Возможно, поколение разработчиков в 2045 году будет изучать квантовую механику наряду с классической информатикой.</p><h2>Инструменты и платформы для начала работы</h2><p>Если не терпится попробовать квантовую разработку, обратите внимание на следующие инструменты.</p><h3>IBM Quantum Experience и Qiskit</h3><figure><img src="https://media.tproger.ru/user-uploads/105601/2025-03-17/cb287521-bb3e-4d57-8566-f69544559aa9.jpg" alt="" /><figcaption>Интерфейс IBM Quantum. Построение схемы квантовой телепортации</figcaption></figure><p><a href="https://quantum.ibm.com/">IBM Quantum Experience</a> — это облачная платформа. Она поддерживает запуск кода на реальном квантовом компьютере — 5-кубитном IBM Quito. Платформа содержит учебные курсы, в том числе серию статей по основам квантовой механики.</p><p>Чтобы программировать кубиты, зарегистрируйтесь на IBM Quantum Experience и познакомьтесь с библиотекой Python Qiskit.</p><p>Пример квантовой схемы:</p><p>В библиотеке Qiskit есть модули для изучения оптимизации, машинного обучения и квантовой химии. Если хотите протестировать программу на реальном образце КК, подключайтесь через API.</p><h3>Google Cirq</h3><p><a href="https://quantumai.google/cirq">Google Cirq</a> — это библиотека Python для создания, выполнения и отладки квантовых вычислений на симуляторах или реальном оборудовании Google. Cirq подходит для тех, кто хочет более программно ориентированный подход к квантовой механике.</p><p>Особенности:</p><ul><li>Cirq задает низкоуровневую структуру операций над кубитами.</li><li>Полностью бесплатная библиотека, поддерживаемая Google.</li><li>Интеграция квантовых и классических инструментов в одном коде.</li></ul><p>Пример квантовой схемы:</p><p>Чтобы использовать физический процессор, нужно подключиться к облаку Google Quantum.</p><figure><img src="https://media.tproger.ru/user-uploads/105601/2025-03-17/fa56a90c-238c-40b7-a6e9-0a5d6fda37c4.jpg" alt="" /><figcaption>Схематическое представление архитектуры квантового процессора Sycamore</figcaption></figure><h3>Microsoft Q#</h3><p><b>Q#</b> — язык с высоким уровнем абстракции. Его используют, чтобы сосредоточиться на алгоритмах без погружения в низкоуровневую обработку кубитов.</p><p>Для работы с Q# понадобится Quantum Development Kit, .NET, расширение для Visual Studio или VS Code.</p><p>Простая программа на Q#:</p><p>Код выполняется через симулятор, встроенный в QDK.</p><p>Microsoft предлагает облако с доступом к реальным квантовым процессорам. После настройки учетной записи Azure можно отправлять программы на лабораторные компьютеры и гибридные симуляторы.</p><h2>Симуляторы</h2><p>Чтобы лучше понять работу квантовых вычислений, стоит начать с симуляторов:</p><ul><li><b>IBM Aer.</b> Входит в состав Qiskit, имитирует шумовые и идеальные квантовые процессы.</li><li><b>Google Cirq’s Simulator</b>. Интегрирован в библиотеку Cirq, поддерживает широкий спектр квантовых операций.</li><li><b>Microsoft Quantum Simulator</b>. Разработан для работы с Q#, позволяет запускать программы и тестировать квантовые алгоритмы.</li><li><b>QuTip</b>. Библиотека Python для работы с квантовыми системами, поддерживает симуляцию квантовых состояний, унитарных операций и вычислений.</li></ul><p>Если вы новичок — используйте Qiskit + IBM Aer.</p><p>Любите низкоуровневое программирование? Google Cirq — ваш выбор.</p><p>Чувствуете, что вам нужен язык высокого уровня с интеграцией в классические приложения? Тогда изучайте Q# + Microsoft Quantum Simulator.</p><h2>Что в итоге?</h2><p>Квантовые компьютеры не заменят классические — они дополнят их, решая специфические задачи. Симулировать кубиты непросто: для будущей интеграции квантовых технологий придется пополнить стек новой библиотекой и пройти курс по квантовой физике.</p><p>IBM, Microsoft и Google развивают экосистемы для новых специалистов. Есть симуляторы и обучающие материалы. Они немного облегчают вход в эту сферу.</p><p>Перед изучением темы нужно признать, что квантовые вычисления — это, без преувеличения, самое сложное направление программирования.</p>]]></content:encoded>
    </item>
    <item>
      <title>Энтузиаст создал игру на чистом ассемблере без использования библиотек</title>
      <link>https://tproger.ru/news/--entuziast-sozdal-igru-na-chistom-assemblere-bez-ispolzovaniya-bibliotek</link>
      <comments>https://tproger.ru/news/--entuziast-sozdal-igru-na-chistom-assemblere-bez-ispolzovaniya-bibliotek?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Булат Яббаров]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/news/--entuziast-sozdal-igru-na-chistom-assemblere-bez-ispolzovaniya-bibliotek</guid>
      <description><![CDATA[<p>Пользователь Reddit под ником LlaroLlethri создал игру на ассемблере для архитектуры x86_64 под Linux, не используя стандартные библиотеки, включая C</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/news/--entuziast-sozdal-igru-na-chistom-assemblere-bez-ispolzovaniya-bibliotek">Энтузиаст создал игру на чистом ассемблере без использования библиотек</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Linux]]></category>
      <category><![CDATA[C++]]></category>
      <category><![CDATA[Игра]]></category>
      <category><![CDATA[Новости]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 02 Oct 2024 06:40:58 GMT</pubDate>
      <content:encoded><![CDATA[<p>Пользователь Reddit под ником LlaroLlethri представил уникальный проект — игру, написанную полностью на ассемблере для архитектуры x86_64 под Linux.</p><p>Особенность этой игры заключается в том, что она разработана без использования каких-либо библиотек, включая стандартную библиотеку C. Это делает её технически сложной и впечатляющей, особенно для современного программирования.</p><h2>Полностью ручной подход к разработке</h2><p>LlaroLlethri решил пойти по необычному пути и отказаться от стандартных инструментов и библиотек, доступных большинству программистов.</p><p>Вместо этого он взаимодействует с системой напрямую, выводя изображение через /dev/fb0 — устройство, отвечающее за отображение на экране. Весь проект написан с нуля, без применения готовых решений.</p><h2>Игровая механика и вдохновение</h2><p>Игра, которую создал разработчик, вдохновлена классической Boulder Dash и представляет собой классический игровой процесс с элементами «добывания» ресурсов.</p><p>Автор подчеркнул, что работа над игрой стала для него обучающим опытом, несмотря на то, что по профессии он работает с C++.</p><h2>Реакция сообщества</h2><p>Проект вызвал активное обсуждение на Reddit.</p><p>Многие пользователи выразили восхищение уровнем погружения в низкоуровневое программирование и поделились своим опытом работы с ассемблером в прошлом.</p><p>Один из пользователей вспомнил, как работал с подобными проектами на Commodore 64, что породило в комментариях цепочку ностальгических воспоминаний о первых шагах в программировании.</p><figure><img src="https://media.tproger.ru/user-uploads/98945/2024-10-02/4625578e-1324-478d-b0f1-5675a4e6d8db.jpg" alt="" /></figure><h2>Доступ к проекту и видео</h2><p>Проект доступен на <a href="https://github.com/robjinman/gemsnrocks_asm">GitHub</a>, где можно скачать исходный код. Кроме того, автор выложил видео на YouTube, демонстрирующее, как игра работает на его ноутбуке:</p>]]></content:encoded>
    </item>
    <item>
      <title>Изобретатели компилятора получат Премию Тьюринга и 1 миллион долларов</title>
      <link>https://tproger.ru/news/izobretateli-kompiljatora-poluchat-premiju-tjuringa-i-1-million-dollarov</link>
      <comments>https://tproger.ru/news/izobretateli-kompiljatora-poluchat-premiju-tjuringa-i-1-million-dollarov?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Булат Яббаров]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/news/izobretateli-kompiljatora-poluchat-premiju-tjuringa-i-1-million-dollarov</guid>
      <description><![CDATA[<p>Ассоциация вычислительной техники наградила Альфреда Ахо и Джеффри Ульмана за создание компилятора и учебники, обучившие поколения студентов.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/news/izobretateli-kompiljatora-poluchat-premiju-tjuringa-i-1-million-dollarov">Изобретатели компилятора получат Премию Тьюринга и 1 миллион долларов</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Компиляторы]]></category>
      <category><![CDATA[Премия Тьюринга]]></category>
      <category><![CDATA[Новости]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 01 Apr 2021 05:00:59 GMT</pubDate>
      <content:encoded><![CDATA[<p>Ассоциация вычислительной техники объявила победителей, которые получат Премию Тьюринга 2021. Ими стали два ветерана индустрии — Джеффри Ульман и Альфред Ахо.</p><figure><img src="https://media.tproger.ru/uploads/2021/04/1.jpg" alt="" /><figcaption>Джеффри Ульман (слева) и Альфред Ахо (справа)</figcaption></figure><p>Организация решила наградить разработчиков за их, пожалуй, главное творение — компилятор. По мнению экспертов, именно его создание позволило современному миру стать таким, какой он есть сейчас.</p><blockquote>Без их разработки мы бы не смогли писать приложения для наших телефонов. У нас бы не было тех машин, на которых мы сегодня ездим.</blockquote><p>Исследователи также написали множество учебников. Они обучили целые поколения студентов тому, как разработка компьютерных программ отличается от электротехники или математики.</p><p>Премия Тьюринга выдаётся с 1966 года. Главным призом, помимо всеобщего признания единомышленников, является денежная награда — 1 млн долларов. Её Ульман с Ахо поделят между собой.</p><p>Источник: <a href="https://www.nytimes.com/2021/03/31/technology/turing-award-aho-ullman.html">The New York Times</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Разработчик создал ОС для устройств, собранных из деталей с постапокалиптических свалок</title>
      <link>https://tproger.ru/news/collapse-os</link>
      <comments>https://tproger.ru/news/collapse-os?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[karpov]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/news/collapse-os</guid>
      <description><![CDATA[<p>ОС Collapse для компьютеров из постапокалиптических свалок использует Z80: редактирование текста, чтение/запись данных, компиляция ассемблера.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/news/collapse-os">Разработчик создал ОС для устройств, собранных из деталей с постапокалиптических свалок</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Операционные системы]]></category>
      <category><![CDATA[Новости]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 10 Oct 2019 12:13:03 GMT</pubDate>
      <content:encoded><![CDATA[<p>На случай апокалипсиса придумали операционную систему под названием Collapse. Её разработчик Вирджил Дупрас говорит, что она сможет работать на компьютерах, собранных вручную из деталей с постапокалиптических мусорных свалок. Проект лежит <a href="https://github.com/hsoft/collapseos">на GitHub</a>, всех желающих приглашают поучаствовать в разработке.</p><p>ОС Collapse может работать с 8-битными микропроцессорами Z80. Они до сих пор используются в кассовых аппаратах, калькуляторах. За всё время их выпустили гораздо больше, чем 16- и 32-битных микропроцессоров. Поэтому их проще будет найти в условиях постапокалипсиса. Дупрас считает, что к 2030 году массовое производство электротехники сойдёт на нет. Ценен будет тот, кому удастся найти и перепрограммировать какое-то старое устройство.</p><p>Сейчас ОС работает на собранном вручную компьютере RC2014. На ней можно редактировать текст, читать и записывать данные и даже компилировать файлы для ассемблера.</p>]]></content:encoded>
    </item>
    <item>
      <title>Реверс-инжиниринг для начинающих: продвинутые концепции программирования</title>
      <link>https://tproger.ru/translations/reverse-engineering-concepts</link>
      <comments>https://tproger.ru/translations/reverse-engineering-concepts?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Евгений Туренко]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/reverse-engineering-concepts</guid>
      <description><![CDATA[<p>Массивы, указатели, динамическое распределение памяти, сокеты и многопоточность в дизассемблированном коде — вторая часть с примерами на IDA Pro.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/reverse-engineering-concepts">Реверс-инжиниринг для начинающих: продвинутые концепции программирования</a>»</p>]]></description>
      <category><![CDATA[Для начинающих]]></category>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Безопасность]]></category>
      <category><![CDATA[Язык Си]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Sun, 31 Mar 2019 08:47:00 GMT</pubDate>
      <content:encoded><![CDATA[<p>В <a href="https://tproger.ru/translations/reverse-engineering-basics/">первой части</a> мы рассмотрели базовые концепции программирования, такие как циклы и условный оператор, в этой статье будем рассматривать более сложные темы, необходимые для реверс-инжиниринга.</p><p>Примечание Для дизассемблирования в этой статье используется IDA Pro, но многие её функции (например блок-схемы, перевод в псевдокод и т. д.) можно найти в качестве надстроек в бесплатных дизассемблерах (radare2). Более того, для лучшего понимания имена некоторых дизассемблированных переменных были изменены с «v20» на имена, которые были у них в С. Также в этой статье исполняемый файл был скомпилирован в 64-битной версии, а для дизассемблирования используется 64-битная версия IDA Pro. Это на случай, если вы захотите повторить всё самостоятельно, потому что это может повлиять на конечный результат (например, на массивах будет сильное различие 32 и 64-битных версий, а также в 64-битной версии регистры становятся в два раза больше).</p><h2>Массивы</h2><p>Итак, начнём с массивов. Сначала рассмотрим код на Си:</p><p>Эти 12 строк кода превращаются в довольно внушительный блок машинного кода. Давайте рассмотрим его детально:</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-2.jpeg" alt="" /></figure><p>При инициализации массива с константным размером компилятор просто инициализирует длину массива через локальную переменную.</p><p>Во время компиляции будет выделено место только под одно значение массива litArray[0], которое и будет использоваться (можно увидеть на скриншоте ниже). Такой приём позволяет компилятору значительно увеличить производительности приложений.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-3.jpeg" alt="" /></figure><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-4.jpeg" alt="" /></figure><p>Сначала длина массива сохраняется в локальную переменную ArraySize, затем вычисляется максимальное и минимальное индексное значение, а также длина всего массива, а затем под неё выделяется память.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-5.jpeg" alt="" /></figure><p>При объявлении массива с предопределёнными значениями компилятор сохраняет каждое значение в свою переменную, которая представлена индексом массива (например objArray4 = objArray[4]).</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-6.jpeg" alt="" /></figure><p>Так же как и с предопределёнными значениями, компилятор создаёт новую переменную для указанного индексного значения при инициализации элемента массива через индекс.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-7.jpg" alt="" /></figure><p>При извлечении элемента массива значение элемента берётся по указанному индексу и записывается в нужную переменную.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic8.jpeg" alt="" /></figure><p>При создании матрицы сначала её размер устанавливается в соответствии со значениями row и col. Затем рассчитываются максимальный и минимальный индексы для строк и столбцов, которые используются для расчёта базового местоположения и общего размера матрицы в памяти.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-9.jpeg" alt="" /></figure><p>При вводе в матрицу сначала определяется местоположение желаемого элемента массива с использованием базового местоположения матрицы. Затем содержимое указанного элемента массива устанавливается на желаемое входное значение (т.е. 1337).</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-10.jpeg" alt="" /></figure><p>При извлечении значения из матрицы происходят такие же вычисления, как и при внесении значения в неё. Однако при этом ничего не записывается — содержимое извлекается и записывается в нужную переменную (например MatrixLeet).</p><h2>Указатели</h2><p>Теперь, когда мы понимаем, как массивы используются и выглядят в машинном коде, давайте перейдём к указателям.</p><p>Давайте сразу разберёмся в машинном коде:</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-11.jpeg" alt="" /></figure><p>Сначала мы присваиваем переменной num значение 10.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-12.jpeg" alt="" /></figure><p>Затем указателю pointer присваивается адрес переменной num.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-13.jpeg" alt="" /></figure><p>Вывод переменной num на экран.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-14.jpeg" alt="" /></figure><p>Вывод переменной pointer на экран.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-15.jpeg" alt="" /></figure><p>Вывод адреса переменной num происходит с помощью инструкции lea (загрузка результирующего адреса) вместо mov.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-16.jpeg" alt="" /></figure><p>Вывод адреса переменной num через указатель pointer.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-17.jpeg" alt="" /></figure><p>Вывод адреса переменной pointer происходит с помощью инструкции lea вместо mov.</p><h2>Динамическое распределение памяти</h2><p>В этой статье будут рассмотрены следующие виды динамического распределения памяти:</p><ol><li>malloc.</li><li>calloc.</li><li>realloc.</li></ol><h3>malloc — динамическое выделение памяти</h3><p>Сначала разберёмся в коде:</p><p>Прим. перев. В оригинале статьи выделяется 11 байтов, хотя правильно будет 12. В конце строки ещё добавляется символ с кодом 0.</p><p>В этой функции выделяется место под 11 символов с помощью malloc(), а затем в выделенное пространство памяти копируется «Hello World».</p><p>Теперь давайте посмотрим на машинный код:</p><p>Примечание Во время сборки вы можете увидеть инструкции «nop». Эти инструкции были специально размещены на этапе подготовки к статье, чтобы различные части кода было проще понимать.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-18.jpeg" alt="" /></figure><p>При использовании malloc() размер выделенной памяти (0x0B) сначала перемещается в регистр edi. Затем системная функция _malloc вызывается для выделения памяти. Выделенная область памяти затем сохраняется в переменной ptr. Потом строка «Hello World» разбивается на «Hello Wo» и «rld», поскольку она копируется в выделенное пространство памяти. Наконец, вновь скопированная строка «Hello World» выводится на экран, а выделенная память освобождается с помощью функции _free.</p><h3>calloc — динамическое чистое выделение памяти</h3><p>Посмотрим на код:</p><p>Как и в методе malloc(), место для 11 символов выделяется, а строка «Hello World» копируется в указанное пространство. Затем вновь перемещённый «Hello World» распечатывается, и выделенное пространство памяти освобождается.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-19.jpeg" alt="" /></figure><p>Динамическое распределение памяти через calloc() выглядит почти идентично динамическому распределению памяти через malloc() в машинном коде.<br />Во-первых, пространство для 11 символов (0x0B) выделяется с помощью системной функции _calloc. Затем строка «Hello World» разбивается на «Hello Wo» и «rld„, поскольку она копируется во вновь выделенную область памяти. Затем вновь перемещённая строка “Hello World» выводится на экран, а выделенная область памяти освобождается с помощью функции _free.</p><h3>realloc — динамическое перераспределение памяти</h3><p>Сначала посмотрим код.</p><p>В этой функции память для 11 символов выделяется с помощью malloc(). Затем «Hello World» копируется в только что выделенное пространство памяти, прежде чем указанное расположение памяти перераспределяется, чтобы соответствовать 21 символу.</p><p>Прим. перев.: Должно быть 22 символа. Снова автор забыл символ с кодом 0, используя realloc()</p><p>Наконец, «1337 h4x0r @nonymoose» копируется в только что перераспределённое пространство. Наконец, после вывода на экран память освобождается.</p><p>Теперь посмотрим машинный код:</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-20.jpg" alt="" /></figure><p>Сначала память выделяется с помощью malloc(). Затем, после вывода на экран только что перемещённой строки «Hello World», вызывается realloc() для переменной ptr (которая представляет переменную mem_alloc в коде), а также передаётся новый размер 0x15 (21 в десятичном виде). Затем «1337 h4x0r @nonymoose» разбивается на «1337 h4x„, “0r @nony», «moos» и «e„, поскольку он копируется в только что перераспределённое пространство памяти. Наконец, пространство освобождается с помощью функции _free.</p><h2>Программирование сокетов</h2><p>Далее мы рассмотрим программирование сокетов, разобрав очень простую систему клиент-серверного TCP-чата.</p><p>Прежде чем мы начнём разбирать код сервера или клиента, важно указать следующую строку кода в верхней части файла:</p><p>Эта строка определяет константу PORT как 1337. Эта константа будет использоваться как на клиенте, так и на сервере в качестве сетевого порта, используемого для создания соединения.</p><h2>Серверная часть</h2><p>Сначала посмотрим на код:</p><p>Сначала создаётся файловый дескриптор сокета server с доменом AF_INET, типом SOCK_STREAM и кодом протокола 0. Далее настраиваются параметры сокета и адрес. Затем сокет привязывается к сетевому адресу (порту), и сервер начинает прослушивать указанный порт с максимальной длиной очереди 3. После получения соединения сервер принимает его в переменную sock и считывает переданное значение в переменную value.</p><p>Наконец, сервер отправляет строку serverhello по соединению до возврата функции.</p><p>Теперь давайте разберём его в машинном коде:</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-21.jpeg" alt="" /></figure><p>Сначала создаются и инициализируются переменные сервера.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-22.jpeg" alt="" /></figure><p>Затем создаётся файловый дескриптор сокетов server с помощью системной функции _socket. Параметры для функции — протокол, тип и доменное имя передаются с помощью регистров edx, esi и edi соответственно.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-23.jpeg" alt="" /></figure><p>Затем вызывается _setsockopt для задания параметров сокета в файле дескриптора “server».</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-24.jpg" alt="" /></figure><p>Инициализируется серверный адрес с помощью adress.sin_family, address.sin_addr.s_addr и address.sin_port.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-25.jpg" alt="" /></figure><p>После того как сервер был сконфигурирован, он привязывается к интернет-адресу с помощью _bind.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-26.jpeg" alt="" /></figure><p>После привязки сервер слушает сокет, передав файловый дескриптор server. Максимальная длина очереди равна 3.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-27.jpg" alt="" /></figure><p>Как только соединение установлено, сервер принимает соединение сокета в переменную sock.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-28.jpeg" alt="" /></figure><p>Затем сервер считывает переданное в переменную value сообщение с помощью _read.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-29.jpeg" alt="" /></figure><p>В конце концов, сервер отсылает сообщение serverhello через переменную s в машинном коде.</p><h2>Клиентская часть</h2><p>Сначала разберёмся в коде:</p><p>Сначала создаётся файловый дескриптор сокета sock с помощью переменной домена AF_INET типа SOCK_STREAM и кода протокола 0. Затем memset используется для заполнения области памяти server_addr нулями до того, как информация об адресе будет установлена ​​с помощью server_addr.sin_family и server_addr.sin_port. До того как клиент подключится к серверу, информация об адресе преобразуется из текстового в двоичный формат с использованием inet_pton. После подключения клиент отправляет строку helloclient и затем принимает ответ сервера в переменную value. Наконец, переменная value выводится на экран, и происходит возврат из функции.</p><p>Теперь разбёремся в машинном коде:</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-29.jpg" alt="" /></figure><p>Сначала инициализируются локальные переменные клиента.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-30.jpeg" alt="" /></figure><p>Дескриптор файла сокета «sock» создается путём вызова системной функции _socket и передачи информации о протоколе, типе и домене через регистры edx, esi и edi соответственно.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-31.jpeg" alt="" /></figure><p>Переменная server_address (в машинном коде «s») заполняется нулями (0x30) с помощью системного вызова _memset.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-32.jpeg" alt="" /></figure><p>Потом настраивается адресная информация сервера.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-33.jpeg" alt="" /></figure><p>Затем адрес переводится из текстового в двоичный формат с помощью системной функции _inet_pton. Обратите внимание, что, поскольку в коде явно не указан адрес, предполагается localhost (127.0.0.1).</p><p>Клиент подключается к серверу с помощью системного вызова _connect.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-35.jpeg" alt="" /></figure><p>После подключения клиент отправляет строку helloClient на сервер.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-36.jpeg" alt="" /></figure><p>Наконец, клиент получает ответ сервера в переменную value с помощью системного вызова _read.</p><h2>Многопоточность</h2><p>Наконец, мы рассмотрим основы потоков в C.</p><p>Во-первых, давайте посмотрим на код:</p><p>Как вы можете видеть, программа сначала печатает «This is before the thread», затем создаёт новый поток, который указывает на функцию *mythread(), используя функцию pthread_create(). По завершении функции *mythread() (после сна длиной в 1 секунду и вывода на экрана «Hello from mythread») новый поток присоединяется к основному потоку с помощью функции pthread_join() и выводится на экран «This is after the thread».</p><p>Теперь давайте разберём машинный код:</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-37.jpeg" alt="" /></figure><p>Сначала программа печатает «This is before the thread».</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-38.jpeg" alt="" /></figure><p>Затем создаётся новый поток с помощью системного вызова _pthread_create. Этот поток получает mythread() в качестве аргумента.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-39.jpeg" alt="" /></figure><p>Как вы можете видеть, функция mythread() просто спит одну секунду перед выводом «Hello from mythread».</p><p>Примечание Внутри функции mythread() вы увидите два нопа. Они были специально размещены для облегчения навигации на этапе подготовки этой статьи.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-40.jpeg" alt="" /></figure><p>После возврата из функции mythread() новый поток соединяется с основным потоком с помощью функции _pthread_join.</p><figure><img src="https://media.tproger.ru/uploads/2019/03/reverse-dev-pic-42.jpeg" alt="" /></figure><p>Наконец, на экран выводится «This is after the thread» и происходит возврат из функции.</p><h2>Заключение</h2><p>В статье мы рассмотрели массивы, указатели, динамическое распределение памяти, программирование сокетов (сетевое программирование) и многопоточность. Понимание этих аспектов существенно поможет вам продвинуться в изучении реверс-инжиниринга.</p>]]></content:encoded>
    </item>
    <item>
      <title>Реверс-инжиниринг для начинающих: основные концепции программирования</title>
      <link>https://tproger.ru/translations/reverse-engineering-basics</link>
      <comments>https://tproger.ru/translations/reverse-engineering-basics?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Андрей Селивестров]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/reverse-engineering-basics</guid>
      <description><![CDATA[<p>Общее представление о том, как исследуют программное обеспечение и читают ассемблерный код: переменные, устройство кода и работа с дизассемблером IDA Pro.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/reverse-engineering-basics">Реверс-инжиниринг для начинающих: основные концепции программирования</a>»</p>]]></description>
      <category><![CDATA[Для начинающих]]></category>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 13 Feb 2019 11:00:57 GMT</pubDate>
      <content:encoded><![CDATA[<p>В этой статье мы заглянем под капот программного обеспечения. Новички в реверс-инжиниринге получат общее представление о самом процессе исследования ПО, общих принципах построения программного кода и о том, как читать ассемблерный код.</p><p>Примечание Программный код для этой статьи компилируется с помощью Microsoft Visual Studio 2015, так что некоторые функции в новых версиях могут использоваться по-другому. В качестве дизассемблера используется IDA Pro.</p><ol><li><a href="https://tproger.ru/#part1">Инициализация переменных</a></li><li><a href="https://tproger.ru/#part2">Стандартная функция вывода</a></li><li><a href="https://tproger.ru/#part3">Математические операции</a></li><li><a href="https://tproger.ru/#part4">Вызов функций</a></li><li><a href="https://tproger.ru/#part5">Циклы</a></li><li><a href="https://tproger.ru/#part6">Условный оператор</a></li><li><a href="https://tproger.ru/#part7">Оператор выбора</a></li><li><a href="https://tproger.ru/#part8">Пользовательский ввод</a></li></ol><h2>Инициализация переменных</h2><p>Переменные — одна из основных составляющих программирования. Они делятся на несколько видов, вот некоторые из них:</p><ul><li>строка;</li><li>целое число;</li><li>логическая переменная;</li><li>символ;</li><li>вещественное число с двойной точностью;</li><li>вещественное число;</li><li>массив символов.</li></ul><p>Стандартные переменные:</p><p>Примечание в С++ строка — не примитивная переменная, но важно понять, как она будет выглядеть в машинном коде.</p><p>Давайте посмотрим на ассемблерный код:</p><p><a href="https://media.tproger.ru/uploads/2019/02/reverse1.jpeg"></a> Инициализация переменных</p><p>Здесь можно увидеть как IDA показывает распределение пространства для переменных. Сначала под каждую переменную выделяется пространство, а потом уже она инициализируется.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse2.jpeg" alt="" /></figure><p>Как только пространство выделено, в него помещается значение, которое мы хотим присвоить переменной. Инициализация большинства переменных представлена на картинке выше, но как инициализируется строка, показано ниже.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse3.jpeg" alt="" /></figure><p>Для инициализации строки требуется вызов встроенной функции.</p><h2>Стандартная функция вывода</h2><p>Примечание Здесь речь пойдёт о том, что переменные помещаются в стек и затем используются в качестве параметров для функции вывода. Концепт функции с параметрами будет рассмотрен позднее.</p><p>Для вывода данных было решено использовать printf(), а не cout.</p><p>Стандартный вывод:</p><p>Теперь посмотрим на машинный код. Сначала строковый литерал:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse4.jpeg" alt="" /></figure><p>Как видите, строковый литерал сначала помещается в стек для вызова в качестве параметра функции printf().</p><p>Теперь посмотрим на вывод одной из переменных:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse5.jpeg" alt="" /></figure><p>Как можно заметить, сначала переменная intvar помещается в регистр EAX, который в свою очередь записывается в стек вместе со строковым литералом %i, используемым для обозначения целочисленного вывода. Эти переменные затем берутся из стека и используются в качестве параметров при вызове функции printf().</p><h2>Математические операции</h2><p>Сейчас мы поговорим о следующих математических операциях:</p><ol><li>Сложение.</li><li>Вычитание.</li><li>Умножение.</li><li>Деление.</li><li>Поразрядная конъюнкция (И).</li><li>Поразрядная дизъюнкция (ИЛИ).</li><li>Поразрядное исключающее ИЛИ.</li><li>Поразрядное отрицание.</li><li>Битовый сдвиг вправо.</li><li>Битовый сдвиг влево.</li></ol><p>Переведём каждую операцию в ассемблерный код:</p><p>сначала присвоим переменной A значение 0A в шестнадцатеричной системе счисления или 10 в десятичной. Переменной B — 0F, что равно 15 в десятичной.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse6.jpeg" alt="" /></figure><p>Для сложения мы используем инструкцию add:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse7.jpeg" alt="" /></figure><p>При вычитании используется инструкция sub:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse8.jpeg" alt="" /></figure><p>При умножении — imul:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse9.jpeg" alt="" /></figure><p>Для деления используется инструкция idiv. Также мы используем оператор cdq, чтобы удвоить размер EAX и результат деления уместился в регистре.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse10.jpeg" alt="" /></figure><p>При поразрядной конъюнкции используется инструкция and:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse11.jpeg" alt="" /></figure><p>При поразрядной дизъюнкции — or:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse12.jpeg" alt="" /></figure><p>При поразрядном исключающем ИЛИ — xor:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse13.jpeg" alt="" /></figure><p>При поразрядном отрицании — not:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse14.jpeg" alt="" /></figure><p>При битовом сдвиге вправо — sar:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse15.jpeg" alt="" /></figure><p>При битовом сдвиге влево — shl:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse16.jpeg" alt="" /></figure><h2>Вызов функций</h2><p>Мы рассмотрим три вида функций:</p><ol><li>Функция, не возвращающая значение (void).</li><li>Функция, возвращающая целое число.</li><li>Функция с параметрами.</li></ol><p>Вызов функций:</p><p>Сначала посмотрим, как происходит вызов функций newfunc() и newfuncret(), которые вызываются без параметров.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse17.jpeg" alt="" /></figure><p>Функция newfunc() просто выводит сообщение «Hello! I’m a new function!»:</p><p><a href="https://media.tproger.ru/uploads/2019/02/reverse18.jpeg"></a> Функция newfunc()</p><p>Эта функция использует инструкцию retn, но только для возврата к предыдущему местоположению (чтобы программа могла продолжить свою работу после завершения функции). Посмотрим на функцию newfuncret(), которая генерирует случайное целое число с помощью функции С++ rand() и затем его возвращает.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse19.jpeg" alt="" /></figure><p>Сначала выделяется место под переменную A. Затем вызывается функция rand(), результат которой помещается в регистр EAX. Затем значение EAX помещается в место, выделенное под переменную A, фактически присваивая переменной A результат функции rand(). Наконец, переменная A помещается в регистр EAX, чтобы функция могла его использовать в качестве возвращаемого параметра. Теперь, когда мы разобрались, как происходит вызов функций без параметров и что происходит при возврате значения из функции, поговорим о вызове функции с параметрами.</p><p>Вызов такой функции выглядит следующим образом:</p><p><a href="https://media.tproger.ru/uploads/2019/02/reverse20.jpeg"></a> Вызов функции с параметрами</p><p>Строки в С++ требуют вызова функции basic_string, но концепция вызова функции с параметрами не зависит от типа данных. Сначала переменная помещается в регистр, затем оттуда в стек, а потом происходит вызов функции.</p><p>Посмотрим на код функции:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse21.jpeg" alt="" /></figure><p>Эта функция берёт строку, целое число и символ и печатает их с помощью функции printf(). Как видите, сначала переменные размещаются в начале функции, затем они помещаются в стек для вызова в качестве параметров функции printf(). Очень просто.</p><h2>Циклы</h2><p>Теперь, когда мы изучили вызов функции, вывод, переменные и математику, перейдём к контролю порядка выполнения кода (flow control). Сначала мы изучим цикл for:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse22.jpeg" alt="" /></figure><p>Прежде чем разбить ассемблерный код на более мелкие части, посмотрим на общий вариант. Как вы можете видеть, когда цикл for запускается, у него есть 2 варианта:</p><ul><li>он может перейти к блоку справа (зелёная стрелка) и вернуться в основную программу;</li><li>он может перейти к блоку слева (красная стрелка) и вернуться к началу цикла for.</li></ul><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse23.jpeg" alt="" /></figure><p>Сначала сравниваются переменные i и max, чтобы проверить, достигла ли переменная максимального значения. Если переменная i не больше или не равна переменной max, то подпрограмма пойдёт по красной стрелке (вниз влево) и выведет переменную i, затем i увеличится на 1 и произойдёт возврат к началу цикла. Если переменная i больше или равна max, то подпрограмма пойдёт по зелёной стрелке, то есть выйдет из цикла for и вернётся в основную программу.</p><p>Теперь давайте взглянем на цикл while:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse24.jpeg" alt="" /></figure><p>В этом цикле генерируется случайное число от 0 до 20. Если число больше 10, то произойдёт выход из цикла со словами «I’m out!», в противном случае продолжится работа в цикле.</p><p>В машинном коде переменная А сначала инициализируется и приравнивается к нулю, а затем инициализируется цикл, A сравнивается с шестнадцатеричным числом 0A, которое равно 10 в десятичной системе счисления. Если А не больше и не равно 10, то генерируется новое случайное число, которое записывается в А, и снова происходит сравнение. Если А больше или равно 10, то происходит выход из цикла и возврат в основную программу.</p><h2>Условный оператор</h2><p>Теперь поговорим об <a href="https://tproger.ru/articles/uslovnye-operatory-v-java/">условных операторах</a>. Для начала посмотрим код:</p><p>Эта функция генерирует случайное число от 0 до 20 и сохраняет получившееся значение в переменной А. Если А больше 15, то программа выведет «greater than 15». Если А меньше 15, но больше 10 — «less than 15, greater than 10». Если меньше 5 — «less than 5».</p><p>Посмотрим на ассемблерный граф:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse25.jpeg" alt="" /></figure><p>Граф структурирован аналогично фактическому коду, потому что условный оператор выглядит просто: «Если X, то Y, иначе Z». Если посмотреть на первую сверху пару стрелок, то оператору предшествует сравнение А с 0F, что равно 15 в десятичной системе счисления. Если А больше или равно 15, то подпрограмма выведет «greater than 15» и вернётся в основную программу. В другом случае произойдёт сравнение А с 0A (1010). Так будет продолжаться, пока программа не выведет что-нибудь на экран и не вернётся.</p><h2>Оператор выбора</h2><p>Оператор выбора очень похож на оператор условия, только в операторе выбора одна переменная или выражение сравнивается с несколькими «случаями» (возможными эквивалентностями). Посмотрим код:</p><p>В этой функции переменная А получает случайное значение от 0 до 10. Затем А сравнивается с несколькими случаями, используя switch. Если значение А равно одному из случаев, то на экране появится соответствующее число, а затем произойдёт выход из оператора выбора и возврат в основную программу.</p><p>Оператор выбора не следует правилу «Если X, то Y, иначе Z» в отличии от условного оператора. Вместо этого программа сравнивает входное значение с существующими случаями и выполняет только тот случай, который соответствует входному значению. Рассмотрим два первых блока подробней.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse26.jpeg" alt="" /></figure><p>Сначала генерируется случайное число и записывается в А. Теперь программа инициализирует оператор выбора, приравняв временную переменную var_D0 к А, затем проверяет, что она равна хотя бы одному из случаев. Если var_D0 требуется значение по умолчанию, то программа пойдёт по зелёной стрелке в секцию окончательного возврата из подпрограммы. Иначе программа совершит переход в нужный case.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse27.jpeg" alt="" /></figure><p>Если var_D0 (A) равно 5, то код перейдёт в секцию, которая показана выше, выведет «5» и затем перейдёт в секцию возврата.</p><h2>Пользовательский ввод</h2><p>В этом разделе мы рассмотрим ввод пользователя с помощью потока сin из C++. Во-первых, посмотрим на код:</p><p>В этой функции мы просто записываем строку в переменную sentence с помощью функции C++ cin и затем выводим предложение с помощью оператора printf().</p><p>Разберём это в машинном коде. Во-первых, функция cin:</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse28.jpeg" alt="" /></figure><p>Сначала происходит инициализация строковой переменной sentence, затем вызов cin и запись введённых данных в sentence.</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse29.jpeg" alt="" /></figure><p>Сначала программа устанавливает содержимое переменной sentence в EAX, затем помещает EAX в стек, откуда значение переменной будет использоваться в качестве параметра для потока cin, затем вызывается оператор потока &gt;&gt;. Его вывод помещается в ECX, который затем помещается в стек для оператора printf():</p><figure><img src="https://media.tproger.ru/uploads/2019/02/reverse30.jpeg" alt="" /></figure><p>Мы рассмотрели лишь основные принципы работы программного обеспечения на низком уровне.  Без этих основ невозможно понимать работу ПО и, соответственно, заниматься его исследованием.</p><p>Смотрите также: <a href="https://tproger.ru/translations/stack-protection-bypass/">Эксплуатация уязвимостей исполняемых файлов для новичков</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Google представила библиотеку cpu_features для определения функций процессора</title>
      <link>https://tproger.ru/news/cpu-features</link>
      <comments>https://tproger.ru/news/cpu-features?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Семён Алексин]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/news/cpu-features</guid>
      <description><![CDATA[<p>Библиотека cpu_features на C определяет поддерживаемые процессором функции и помогает выбирать инструкции для оптимизации программ на разных архитектурах.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/news/cpu-features">Google представила библиотеку cpu_features для определения функций процессора</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Google]]></category>
      <category><![CDATA[Новости]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 07 Feb 2018 21:57:39 GMT</pubDate>
      <content:encoded><![CDATA[<p>Для разработчиков, желающих получить максимум производительности, очень важны аппаратные методы, позволяющие оптимизировать код. Но ранее не было способа узнать точно, какие методы и технологии процессор поддерживает. Библиотека cpu_features написана, чтобы решить эту проблему. Используя ее, можно прямо из кода узнать, поддерживает ли процессор ту или иную функцию. Написанная на C, эта библиотека может быть использована в любых проектах. При этом сами авторы заявляют, что cpu_features не выделяет себе памяти и работает в изолированном потоке.</p><h3>Возможности cpu_features</h3><p>Не все процессоры имеют одинаковый набор функций. Существуют индивидуальные методы, или даже наборы инструкций, позволяющие улучшить производительность программы, но доступные лишь на определенных архитектурах процессоров. Например, процессоры Intel с архитектурой Haswell поддерживают набор инструкций AVX2. А вот архитектура Sandy Bridge не имеет такой функции. В таком случае на помощь придет cpu_features, способная определить доступные функции за пару строк кода. Это будет очень полезно для разработчиков, старающихся максимально ускорить и оптимизировать свой код, особенно если это драйвер для устройства или новый видеокодек.</p><p>На момент написания статьи библиотека поддерживает процессоры x86, ARM, AArch64 и MIPS. Более детально о поддерживаемых архитектурах и технологиях ниже:</p><figure><img src="https://media.tproger.ru/uploads/2018/02/cpu-features-table.png" alt="" /></figure><p>Исходный код библиотеки и документация <a href="https://github.com/google/cpu_features">доступны</a> на GitHub.</p>]]></content:encoded>
    </item>
    <item>
      <title>Выпущена версия DOOM, работающая только на mov-инструкциях</title>
      <link>https://tproger.ru/news/doom-movfuscated</link>
      <comments>https://tproger.ru/news/doom-movfuscated?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Алексей Михайлишин]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/news/doom-movfuscated</guid>
      <description><![CDATA[<p>Патч M/o/Vfuscator компилирует DOOM только с инструкцией mov в ассемблере, используя её тьюринг-полноту вместо условных операторов.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/news/doom-movfuscated">Выпущена версия DOOM, работающая только на mov-инструкциях</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Язык ассемблера]]></category>
      <category><![CDATA[Чипокалипсис]]></category>
      <category><![CDATA[Новости]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 25 Jan 2018 13:27:15 GMT</pubDate>
      <content:encoded><![CDATA[<p>Разработчики инструмента <a href="https://tproger.ru/articles/movfuscator/">M/o/Vfuscator</a> выпустили <a href="https://github.com/xoreaxeaxeax/movfuscator/tree/master/validation/doom">патч</a> для классической компьютерной игры DOOM, который позволяет скомпилировать игру без использования условных операторов и вообще каких-либо инструкций, кроме <a href="https://en.wikipedia.org/wiki/MOV_(x86_instruction)">mov</a> в языке ассемблера. Сделано это для доказательства работоспособности и применимости их инструмента. При этом не применяется никаких трюков с самомодифицирующимся кодом или подобных — всё работает честно.</p><h3>Как такое возможно?</h3><p>Оказывается, сама по себе инструкция mov <a href="https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D0%BD%D0%BE%D1%82%D0%B0_%D0%BF%D0%BE_%D0%A2%D1%8C%D1%8E%D1%80%D0%B8%D0%BD%D0%B3%D1%83">тьюринг-полная</a>, а значит, с её помощью можно запрограммировать любой алгоритм, как на классических языках.</p><p>Если коротко, то суть в том, как проводить «сравнения» и затем в зависимости от результата выполнять определённую команду. Допустим, мы хотим сравнить значения A и B. Помещаем их в регистры Ri и Rj. Мы это можем сделать, у нас есть mov. Затем выполняем хитрый код:</p><p>Здесь мы интерпретируем значения Ri и Rj (т.е. A и B) как адреса и записываем в них 0 и 1. Если значения были одинаковые, то и адрес будет одинаковый, и он перезапишется. В итоге в Rk будет 1, если A=B, и 0 в другом случае. Почему нам не важно, что было по адресам Ri и Rj — читайте в подробном разборе, для нас это сейчас не имеет значения.</p><p>Затем мы можем поместить по некоторому адресу N одну инструкцию, а в следующую ячейку памяти N+1 — другую. И выполнить ту, что по адресу N+Rk. Вот мы и получили аналог условного выполнения инструкций, но используя только mov. Осталось только перевести весь код в такую форму, а в самом конце добавить безусловный переход на начало программы.</p><p>Более детально изучить, как это работает, можно по <a href="https://www.youtube.com/watch?v=R7EEoWg6Ekk">презентации</a>, <a href="https://github.com/xoreaxeaxeax/movfuscator/blob/master/slides/domas_2015_the_movfuscator.pdf">слайдам</a> или текстовому описанию в PDF.</p><h3>Зачем это нужно?</h3><p>В некотором роде подобные схемы уже реализуются в <a href="https://ru.wikipedia.org/wiki/RISC">RISC-архитектуре</a>. Иначе говоря, идея не новая и позволяет выпускать более простые компиляторы и процессоры, которые по этой причине могут работать быстрее и быть дешевле. Кроме того, такой код защищён от <a href="https://tproger.ru/articles/meltdown-spectre-vulnerabilities-overview/">недавних уязвимостей в процессорах</a> просто по определению, т.к. в нём нет спекулятивного выполнения инструкций.</p><p>Для авторов M/o/Vfuscator сборка DOOM только через mov-инструкции нужна для доказательства работоспособности их метода. Работает, правда, всё очень долго — один кадр рендерится около 7 часов. Но в данном случае главное, что вообще работает.</p>]]></content:encoded>
    </item>
    <item>
      <title>«Шелл» на С: пишем командную оболочку для Unix</title>
      <link>https://tproger.ru/translations/unix-shell-in-c</link>
      <comments>https://tproger.ru/translations/unix-shell-in-c?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Ярослав Сарницкий]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/unix-shell-in-c</guid>
      <description><![CDATA[<p>Многие считают, что сделать программу, которой будут пользоваться миллионы, очень трудно. Однако за любым, даже самым сложным, продуктом всегда стоит простая идея. Одним из них является командная оболочка, или «шелл». В этой статье мы расскажем, как написать упрощенную командную оболочку Unix на C.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/unix-shell-in-c">«Шелл» на С: пишем командную оболочку для Unix</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Язык Си]]></category>
      <category><![CDATA[Для продолжающих]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 06 Sep 2017 18:10:26 GMT</pubDate>
      <content:encoded><![CDATA[<p>Многие считают, что сделать программу, которой будут пользоваться миллионы, очень трудно. Однако за любым, даже самым сложным, продуктом всегда стоит простая идея. Одним из них является командная оболочка, или «шелл». В этой статье мы расскажем, как написать упрощенную командную оболочку Unix на C.</p><p>Совет Не стоит сдавать или использовать (даже в изменённом виде) приведённый ниже код в качестве домашнего проекта в школе или вузе. Многие преподаватели знают об оригинальной статье и уличат вас в обмане.</p><h2>Жизненный цикл командной оболочки</h2><p>Оболочка выполняет три основные операции за время своего существования:</p><ol><li>Инициализация: на этом этапе она читает и исполняет свои файлы конфигурации. Они изменяют её поведение.</li><li>Интерпретация: далее оболочка считывает команды из stdin и исполняет их.</li><li>Завершение: после исполнения основных команд она исполняет команды выключения, освобождает память и завершает работу.</li></ol><p>Именно эти три операции мы будем использовать как основу для нашей командной оболочки. Мы не будем добавлять дополнительные файлы конфигурации и команду выключения. Будем лишь вызывать функцию цикла и завершать работу. Стоит отметить, что, с точки зрения архитектуры, жизненный цикл сложнее, чем просто цикл.</p><p>В примере выше можно увидеть функцию lsh_loop(), которая будет циклически интерпретировать команды. Реализацию рассмотрим чуть ниже.</p><h2>Базовый цикл командной оболочки</h2><p>В первую очередь нам нужно подумать о том, как программа должна запускаться. И здесь важно понимать, что делает оболочка во время цикла. Простой способ обработки команд состоит из трех шагов:</p><ol><li>Чтение: считывание команды со стандартных потоков.</li><li>Парсинг: распознавание программы и аргументов во входной строке.</li><li>Исполнение: запуск распознанной команды.</li></ol><p>Эта идея реализована в функции lsh_loop():</p><p>Пройдемся по коду. Первые несколько строк — это просто объявления. Цикл с постусловием более удобен для проверки состояния переменной, поскольку выполняется перед проверкой ее значения. Внутри цикла выводится приглашение ввода, вызываются функции для чтения входной строки и разбиения строки на аргументы, а затем исполняются аргументы. Далее освобождается память, выделенная под строку и аргументы. Стоит обратить внимание, что в коде используется переменная состояния, возвращаемая в lsh_execute() и определяющая, когда нужно выйти из функции.</p><h2>Чтение строки</h2><p>Чтение строки из стандартного потока ввода — это вроде бы просто, но в C это может вызвать много хлопот. Беда в том, что никто не знает заранее, сколько текста пользователь введет в командную оболочку. Нельзя просто выделить блок и надеяться, что пользователи не выйдут за него. Вместо этого нужно перераспределять выделенный блок памяти, если пользователи выйдут за его пределы. Это стандартное решение в C, и именно оно будет использоваться для реализации lsh_read_line().</p><p>В первой части много объявлений. Стоит отметить, что в коде используется старый стиль C, а именно объявление переменных до основной части кода. Основная часть функции находится внутри, на первый взгляд, бесконечного цикла while(1). В цикле символ считывается и сохраняется как int, а не char (EOF — это целое число, а не символ, поэтому для проверки используйте int). Если это символ перевода строки или EOF, мы завершаем текущую строку и возвращаем ее. В обратном случае символ добавляется в существующую строку.</p><p>Затем мы проверяем, выходит ли следующий символ за пределы буфера. Если это так, то перераспределяем буфер (при этом проверяем его на наличие ошибок распределения) и продолжаем исполнение.</p><p>Те, кто знаком с новыми версиями стандартной библиотеки C, могут заметить, что в stdio.h есть функция getline(), которая выполняет большую часть работы, реализованной в коде выше. Эта функция была расширением GNU для библиотеки C до 2008 года, а затем была добавлена в спецификацию, поэтому большинство современных Unix-систем уже идут с ней в комплекте.  С getline функция становится тривиальной:</p><h2>Парсинг строки</h2><p>Теперь нам нужно распарсить входную строку в список аргументов. Мы сделаем небольшое упрощение и запретим пользователю использовать кавычки и обратную косую черту в аргументах командной строки. Вместо этого для разделения аргументов мы просто будем использовать пробелы. Таким образом команда  echo "вот сообщение" будет вызывать команду echo не с одним аргументом "вот сообщение", а с двумя:"вот" и "сообщение".</p><p>Теперь всё, что нам нужно сделать — разбить строку на части, используя пробелы в качестве разделителей. Это значит, что мы можем использовать классическую библиотечную функцию strtok.</p><p>Реализация этой функции подозрительно похожа на lsh_read_line(), и это неспроста! Здесь используется та же стратегия, только вместо нуль-терминированного массива символов мы используем нуль-терминированный массив указателей.</p><p>Мы начинаем разбиение, вызывая strtok. Она возвращает указатель на первый кусок строки (токен). Вообще strtok() возвращает указатели на места в строке и помещает нуль-терминаторы в конце каждого токена. Эти указатели мы храним в отдельном массиве.</p><p>При необходимости мы перераспределим массив указателей. Повторяем процесс до тех пор, пока strtok не перестанет возвращать токены, и завершаем массив токенов нуль-терминатором.</p><p>Теперь у нас есть массив токенов, готовых к исполнению.</p><h2>Как командные оболочки запускают процессы</h2><p>Теперь мы добрались до самой сути того, что делает оболочка. Запуск процессов — это основная функция командных оболочек. Поэтому если вы создаёте оболочку, то должны точно знать, что происходит с процессами и как они запускаются. Именно поэтому сейчас мы поговорим о <a href="https://tproger.ru/translations/unix-basics/">процессах в Unix</a>.</p><p>В Unix есть только два способа запуска процессов. Первый (который не будем брать в счет) — это Init. Видите ли, когда загружается Unix-система, загружается её ядро. После загрузки и инициализации ядро запускает только один процесс, который называется Init. Этот процесс выполняется в течение всего времени работы компьютера, и  управляет загрузкой остальных процессов, которые необходимы для его работы.</p><p>Поскольку все остальные процессы не Init, остаётся только один практический способ запуска процессов: системный вызов fork(). Когда эта функция вызывается, операционная система делает дубликат процесса и запускает их параллельно. Первоначальный процесс называется «родительским», а новый — «дочерним». Дочернему процессу fork() возвращает 0, а родителю — идентификатор процесса (PID) его дочернего элемента. Таким образом, любой новый процесс можно создать только из копии уже существующего.</p><p>Это может показаться проблемой. Обычно, когда вы хотите запустить новый процесс, вам не нужна копия уже работающей программы — вы хотите запустить другую программу. Для этого нужно использовать системный вызов exec(). Он заменяет текущую запущенную программу совершенно новой. Это значит, что при вызове exec операционная система останавливает процесс, загружает новую программу и запускает ее на том же месте. Вызов exec() не возвращает процесс, если нет ошибки.</p><p>Благодаря этим двум системным вызовам и возможен запуск большинства программ в Unix. Сперва существующий процесс раздваивается на родительский и дочерний, а затем дочерний процесс использует exec() для замены себя новой программой. Родительский процесс может продолжать делать другие вещи, а также следить за своими дочерними элементами, используя системный вызов wait().</p><p>Да уж, информации немало. Давайте посмотрим на код запуска программы:</p><p>Эта функция принимает список аргументов, которые мы создали ранее. Затем она разворачивает процесс и сохраняет возвращаемое значение. Как только fork() возвращает значение, мы получаем два параллельных процесса. Дочернему процессу соответствует первое условие if (где pid == 0).</p><p>В дочернем процессе мы хотим запустить команду, заданную пользователем. Поэтому мы используем один из вариантов системного вызова exec, execvp. Разные варианты exec делают разные вещи. Одни принимают переменное количество строковых аргументов, другие берут список строк, а третьи позволяют указать окружение, в котором выполняется процесс. Этот конкретный вариант принимает имя программы и массив (также называемый вектором, отсюда 'v') строковых аргументов (первым должно быть имя программы). 'p' означает, что вместо предоставления полного пути к файлу программы для запуска мы укажем только её имя, а также скажем операционной системе искать её самостоятельно.</p><p>Если команда exec возвращает -1 (или любое другое значение), значит, произошла ошибка. Таким образом, мы используем perror для вывода сообщения об ошибке вместе с именем программы, чтобы было понятно, где произошла ошибка. Затем мы завершаем процесс, но так, чтобы программная оболочка продолжала работать.</p><p>Второе условие (pid &lt; 0) проверяет, произошла ли в процессе выполнения fork() ошибка. Если ошибка есть, мы выводим сообщение об этом на экран, но программа продолжает работать.</p><p>Третье условие означает, что вызов fork() выполнен успешно. Там находится родительский процесс. Мы знаем, что потомок собирается исполнить процесс, поэтому родитель должен дождаться завершения команды. Мы используем waitpid() для ожидания изменения состояния процесса. К сожалению, у waitpid() есть много опций (например, exec()). Процессы могут изменять свое состояние множеством способов, и не все состояния означают, что процесс завершился. Процесс может либо завершиться обычным путём (успешно либо с кодом ошибки), либо быть остановлен сигналом. Таким образом, мы используем макросы, предоставляемые waitpid(), чтобы убедиться, что процесс завершен. Затем функция возвращает 1 как сигнал вызывающей функции, что она снова может вывести приглашение ввода.</p><h2>Встроенные функции оболочки</h2><p>Возможно, вы заметили, что функция lsh_loop() вызывает lsh_execute(), но выше мы назвали нашу функцию lsh_launch(). Это было намеренно! Дело в том, что большинство команд, которые исполняет оболочка, являются программами — но не все. Некоторые из команд встроены прямо в оболочку.</p><p>Причина довольно проста. Если вы хотите сменить каталог, вам нужно использовать функцию chdir(). Дело в том, что текущий каталог является свойством процесса. Итак, допустим, вы написали программу cd, которая изменяет каталог. Она просто меняет свой текущий каталог и завершается, но текущий каталог родительского процесса не изменится. Вместо этого процесс оболочки должен исполнить chdir(), чтобы обновить свой текущий каталог. Затем, когда он запускает дочерние процессы, они также наследуют этот каталог.</p><p>Аналогично программа с именем exit не сможет выйти из командной оболочки, которая ее вызвала. Эта команда также должна быть встроена в оболочку. Кроме того, большинство оболочек настраиваются с помощью сценариев конфигурации, таких как ~/.bashrc. Эти сценарии используют команды, которые изменяют работу оболочки. Сами же команды могут изменить работу оболочки, если только они были реализованы внутри самой оболочки.</p><p>Соответственно, имеет смысл добавить некоторые команды в оболочку. В эту оболочку мы добавим cd, exit и help. А вот и реализация этих функций:</p><p>Код состоит из трёх частей. Первая часть содержит предваряющее объявление функций. Предваряющее объявление — это когда вы объявляете (но не определяете) что-то, чтобы можно было использовать это имя до его определения.lsh_help() — причина, по которой мы делаем это. Она использует массив встроенных функций, а сами массивы содержат lsh_help(). Самый простой способ разбить этот цикл зависимостей — это предваряющее объявление.</p><p>Следующая часть представляет собой массив имён встроенных команд, за которыми следует массив соответствующих функций. Это значит, что в будущем встроенные команды могут быть добавлены путем изменения этих массивов, а не большого оператора switch где-то в коде. Если вы смущены объявлением builtin_func, все в порядке. Это массив указателей на функции (которые принимают массив строк и возвращают int). Любое объявление, включающее указатели на функции в C, может стать действительно сложным.</p><p>Наконец, идет реализация каждой функции. Функция lsh_cd() сначала проверяет наличие своего второго аргумента и выводит сообщение об ошибке, если его нет. Затем она вызывает chdir(), проверяет наличие ошибок и завершает работу. Функция справки выводит информативное сообщение и имена всех встроенных функций. А функция выхода возвращает 0, как сигнал для окончания цикла команд.</p><h2>Объединение встроенных функций и процессов</h2><p>Последний недостающий фрагмент головоломки заключается в реализации функции lsh_execute(), которая либо запускает либо встроенный, либо другой процесс.</p><p>Код проверяет, является ли команда встроенной. Если это так, то запускает её, а в противном случае вызывает lsh_launch(), чтобы запустить процесс.</p><h2>Собираем все вместе</h2><p>Вот и весь код, который входит в командную оболочку. Если вы внимательно читали статью, то должны были понять, как работает оболочка. Чтобы испробовать оболочку (на Linux), вам нужно скопировать эти сегменты кода в файл main.c и скомпилировать его. Обязательно включите только одну реализацию lsh_read_line(). Вам нужно будет включить следующие заголовочные файлы:</p><ul><li>#include &lt;sys/wait.h&gt;waitpid() и связанные макросы</li><li>#include &lt;unistd.h&gt;chdir() fork() exec() pid_t</li><li>#include &lt;stdlib.h&gt;malloc() realloc() free() exit() execvp() EXIT_SUCCESS, EXIT_FAILURE</li><li>#include &lt;stdio.h&gt;fprintf() printf() stderr getchar() perror()</li><li>#include &lt;string.h&gt;strcmp() strtok()</li></ul><p>Чтобы скомпилировать файл, введите в терминале gcc -o main main.c, а затем ./main, чтобы запустить.</p><p>Кроме того, все исходники доступны <a href="https://github.com/brenns10/lsh/tree/407938170e8b40d231781576e05282a41634848c">на GitHub</a>.</p><h2>Подводя итоги</h2><p>Очевидно, что эта оболочка не является многофункциональной. Некоторые из ее упущений:</p><ul><li>аргументы разделяются только пробелами, нет поддержки кавычек или обратного слеша;</li><li>нет перенаправления и конвейеров;</li><li>мало встроенных функций;</li><li>нет подстановки имён файлов.</li></ul><p>Чтобы разобраться в системных вызовах, рекомендуем обратиться к мануалу: man 3p. Если вы не знаете, какой интерфейс вам предлагают стандартная библиотека C и Unix, советуем посмотреть <a href="http://pubs.opengroup.org/onlinepubs/9699919799/">спецификацию POSIX</a>, в частности раздел 13.</p>]]></content:encoded>
    </item>
    <item>
      <title>Компилятор Си в одну инструкцию: обзор инструмента M/o/Vfuscator</title>
      <link>https://tproger.ru/articles/movfuscator</link>
      <comments>https://tproger.ru/articles/movfuscator?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Саша Ушатинская]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/movfuscator</guid>
      <description><![CDATA[<p>Инструмент собирает программы только из инструкций mov, ориентирован на язык Си и архитектуру x86, но настраивается под другие языки и архитектуры.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/movfuscator">Компилятор Си в одну инструкцию: обзор инструмента M/o/Vfuscator</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Open Source]]></category>
      <category><![CDATA[Инструменты]]></category>
      <category><![CDATA[Язык Си]]></category>
      <category><![CDATA[Язык ассемблера]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 30 Mar 2017 13:30:58 GMT</pubDate>
      <content:encoded><![CDATA[<p><a href="https://github.com/xoreaxeaxeax/movfuscator">M/o/Vfuscator</a> компилирует программы в инструкции mov, и только в них. Инструмент ориентирован на язык Си и архитектуру процессора x86, но адаптивен и легко настраивается под другие языки и архитектуры.</p><h3>Демонстрация</h3><p>Компиляция функции, вычисляющей простые числа, с помощью M/o/Vfuscator в сравнении с GCC:</p><h4>Язык Ассемблера</h4><figure><img src="https://media.tproger.ru/uploads/2017/03/1-5.png" alt="" /></figure><p>GCC</p><figure><img src="https://media.tproger.ru/uploads/2017/03/2-4.png" alt="" /></figure><p>M/o/Vfuscator</p><h4>Граф потока управления</h4><figure><img src="https://media.tproger.ru/uploads/2017/03/3-2.png" alt="" /></figure><p>GCC</p><figure><img src="https://media.tproger.ru/uploads/2017/03/4-3.png" alt="" /></figure><p>M/o/Vfuscator</p><h4>M/o/Vfuscator в действии</h4><figure><img src="https://media.tproger.ru/uploads/2017/03/5.gif" alt="" /><figcaption>movcc prime.c -o prime</figcaption></figure><h4>Пример поинтереснее</h4><figure><img src="https://media.tproger.ru/uploads/2017/03/6.gif" alt="" /><figcaption>movcc nibbles.c -o nibbles -lncurses</figcaption></figure><h3>Сборка</h3><p>M/o/Vfuscator использует LCC в качестве препроцессора. Предлагаемый скрипт сборки автоматически загружает LCC, настраивает его конфигурацию для MOV и собирает M/o/Vfuscator.</p><p>Если вы проводите сборку на 64-битной системе, убедитесь, что у вас есть доступ к 32-битной стандартной библиотеке (например, apt-get install libc6-dev-i386 или yum install glibc-devel.i686).</p><h3>Тестирование</h3><p>Проверьте, что сборка произошла корректно, с помощью следующего скрипта:</p><h3>Использование</h3><p>Компилируйте программы как обычно.</p><p>Всю остальную необходимую информацию <a href="https://github.com/xoreaxeaxeax/movfuscator">можно найти</a> в репозитории проекта на GitHub.</p>]]></content:encoded>
    </item>
    <item>
      <title>Как создать свою операционную систему с нуля</title>
      <link>https://tproger.ru/books/create-os</link>
      <comments>https://tproger.ru/books/create-os?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Тарас Сереванн]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/books/create-os</guid>
      <description><![CDATA[<p>Книга «Операционная система с 0 до 1» опубликована на GitHub, собрала больше 2000 звёздочек и 100 форков и учит писать свою ОС самостоятельно.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/books/create-os">Как создать свою операционную систему с нуля</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Операционные системы]]></category>
      <category><![CDATA[Книги]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Fri, 03 Mar 2017 10:57:42 GMT</pubDate>
      <content:encoded><![CDATA[<p>Книга «Операционная система с 0 до 1» <a href="https://github.com/tuhdo/os01">опубликована на GitHub</a> и имеет более 2 000 звездочек и 100 форков. Как понятно из названия, прочитав её, вы сможете создать свою собственную операционную систему — и, пожалуй, мало что в мире программистов может быть круче.</p><h4>Благодаря этой книге вы научитесь следующему:</h4><ul><li>Узнаете, как создать операционную систему на основе технической документации железа. В реальном мире это так и работает, вы не сможете использовать Google для быстрых ответов.</li><li>Поймёте, как компьютерные компоненты взаимодействуют друг с другом, от софта к железу.</li><li>Научитесь писать код самостоятельно. Слепое копирование кода не есть обучение, вы действительно научитесь решать проблемы. Кстати, слепое копирование <a href="https://tproger.ru/translations/dont-copy-paste-from-internet-to-terminal/">может быть</a> еще и опасно.</li><li>Освоите всем привычные инструменты для низкоуровневой разработки.</li><li>Познакомитесь с языком ассемблера.</li><li>Выясните, из чего состоят программы и как операционная система запускает их. Небольшой обзор этой темы для любознательных мы давали в <a href="https://tproger.ru/translations/unix-basics/">одном из наших материалов</a>.</li><li>Разберётесь, как проводить отладку программы прямо на железе с GDB и QEMU.</li></ul><p>Если читать целую книгу у вас нет времени, но вы хотите получить краткий экскурс, прочтите две наших статьи, посвященных созданию ядра для собственной ОС: <a href="https://tproger.ru/translations/lets-write-a-kernel-1/">часть 1</a> и <a href="https://tproger.ru/translations/lets-write-a-kernel-2/">часть 2</a>.</p><h4>Что рекомендуется знать перед прочтением книги:</h4><ul><li>Язык программирования C. Быстро освоить его можно, следуя <a href="https://tproger.ru/digest/c-for-beginners/">нашему руководству</a>.</li><li>Базовые знания Linux. Достаточно изучить <a href="https://tproger.ru/tag/linux/">соответствующую рубрику</a> на нашем сайте.</li><li>Базовые знания в физике: атомы, электроны, протоны, нейтроны, напряжение.</li><li><a href="https://ru.wikipedia.org/wiki/Закон_Ома">Закон Ома</a> о соотношении напряжения, силы тока и сопротивления.</li></ul><p>Книга совершенствуется и редактируется почти каждый день: вы и сами можете внести изменения или исправить опечатку. Операционная система, разработка которой послужила “сюжетом” для этой книги, <a href="https://github.com/tuhdo/sample-os">опубликована</a> на GitHub и также со временем улучшается.</p>]]></content:encoded>
    </item>
    <item>
      <title>Представлена онлайн-IDE для разработки под восьмибитные консоли</title>
      <link>https://tproger.ru/news/8-bit-retro-game-ide</link>
      <comments>https://tproger.ru/news/8-bit-retro-game-ide?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Иван Бирюков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/news/8-bit-retro-game-ide</guid>
      <description><![CDATA[<p>Среда от команды 8bitworkshop позволяет писать и запускать 6502-код прямо в браузере, а также изучать готовые примеры программ для ретро-консолей.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/news/8-bit-retro-game-ide">Представлена онлайн-IDE для разработки под восьмибитные консоли</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Ретро]]></category>
      <category><![CDATA[IDE]]></category>
      <category><![CDATA[Новости]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 28 Dec 2016 16:57:57 GMT</pubDate>
      <content:encoded><![CDATA[<p>Совсем недавно команда 8bitworkshop <a href="http://8bitworkshop.com/">представила</a> среду разработки под ретро-консоли. Пока что она поддерживает только Atari 2600/VCS.</p><figure><img src="https://media.tproger.ru/uploads/2016/12/ide-1024x597.png" alt="" /></figure><p>На сайте можно написать исполняемый 6502-код и тут же запустить его, либо изучить любой из многочисленных образцов программ: например, можно увидеть код типичной <a href="http://8bitworkshop.com/?platform=vcs&amp;file=examples%2Fscoreboard">таблицы результатов</a> или <a href="http://8bitworkshop.com/?platform=vcs&amp;file=examples%2Fhello">реализацию стартовой заставки</a>. Из более сложных примеров стоит отметить <a href="http://8bitworkshop.com/?platform=vcs&amp;file=examples%2Fbrickgame">подобие арканоида</a> и <a href="http://8bitworkshop.com/?platform=vcs&amp;file=examples%2Froad">3D-дорогу</a> (на скриншоте выше).</p>]]></content:encoded>
    </item>
    <item>
      <title>Хитрости с битовыми операциями</title>
      <link>https://tproger.ru/articles/awesome-bits</link>
      <comments>https://tproger.ru/articles/awesome-bits?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Иван Бирюков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/awesome-bits</guid>
      <description><![CDATA[<p>Приёмы работы с битами: установка, обнуление и переключение n-го бита, округление до степени двойки, умножение и деление на 2 и проверка на равенство.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/awesome-bits">Хитрости с битовыми операциями</a>»</p>]]></description>
      <category><![CDATA[Поразрядная обработка]]></category>
      <category><![CDATA[Красивый хак]]></category>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 21 Dec 2016 17:04:02 GMT</pubDate>
      <content:encoded><![CDATA[<p>Нашли и оформили подборку различных хитрых приёмов с битовыми операциями. С основами битовых операций также можно ознакомиться <a href="https://tproger.ru/translations/bitwise-operations/">в нашей статье</a>. Если вы знаете какие-то трюки, которые не вошли в этот список, делитесь ими в комментариях!</p><h3>Целые числа</h3><p>Установка n-ого бита</p><p>Обнуление n-ого бита</p><p>Переключение n-ого бита</p><p>Округление до следующей степени двойки</p><p>Получение максимального целого</p><p>Получение минимального целого</p><p>Получение максимального long</p><p>Умножение на 2</p><p>Деление на 2</p><p>Умножение на m-тую степень 2</p><p>Деление на m-тую степень 2</p><p>Проверка на равенство</p><p>На 35% быстрее, чем JS 🙂</p><p>Проверка на чётность</p><p>Обмен значениями</p><p>Модуль</p><p>Максимум двух чисел</p><p>Минимум двух чисел</p><p>Проверка на одинаковый знак</p><p>Смена знака</p><p>Вычисление 2^n</p><p>Проверка на степень 2</p><p>Остаток от деления 2^n на m</p><p>Среднее арифметическое</p><p>Получение m-ого бита n (справа налево)</p><p>Установка m-ого бита n (справа налево)</p><p>Проверка n-ого бита</p><p>Выделение самой правой 1</p><p>Выделение самого правого 0</p><p>Смена самого правого 0 на 1</p><p>n+1</p><p>n-1</p><p>if (x==a) x=b; if (x==b) x=a;</p><h3>Строки</h3><p>Преобразование буквы в строчную</p><p>Преобразование буквы в заглавную</p><p>Смена вида буквы</p><p>Номер буквы в алфавите</p><p>Номер заглавной буквы в алфавите</p><p>Номер строчной буквы в алфавите</p><h3>Прочее</h3><p>Быстрая смена цвета с R5G5B5 на R8G8B8</p><p>Примечание: можно использовать только латиницу.</p>]]></content:encoded>
    </item>
    <item>
      <title>Руководство по созданию ядра для x86-системы. Часть 2. Система ввода / вывода</title>
      <link>https://tproger.ru/translations/lets-write-a-kernel-2</link>
      <comments>https://tproger.ru/translations/lets-write-a-kernel-2?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Иван Бирюков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/lets-write-a-kernel-2</guid>
      <description><![CDATA[<p>Arjun Sreedharan подключает к ядру драйвер клавиатуры, который считывает символы a–z и 0–9 и выводит их на экран через порты ввода-вывода x86.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/lets-write-a-kernel-2">Руководство по созданию ядра для x86-системы. Часть 2. Система ввода / вывода</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Операционные системы]]></category>
      <category><![CDATA[Язык Си]]></category>
      <category><![CDATA[Язык ассемблера]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Sun, 18 Dec 2016 18:15:52 GMT</pubDate>
      <content:encoded><![CDATA[<p>Рассказывает Arjun Sreedharan</p><p>В <a href="https://tproger.ru/translations/lets-write-a-kernel-1/">прошлой статье</a> я писал о том, как создать простейшее x86-ядро, использующее GRUB, работающее в защищённом режиме и выводящее на экран строку. В этот раз мы подключим к ядру драйвер клавиатуры, который может считывать символы a–z и 0–9 с клавиатуры и выводить их на экран. Весь используемый код можно найти <a href="http://github.com/arjun024/mkeykernel">на GitHub</a>.</p><figure><img src="https://media.tproger.ru/uploads/2016/12/tumblr_inline_ncyxrtFyJm1rivrqc.jpg" alt="" /></figure><p>Мы общаемся с устройствами ввода / вывода, используя <b>I/O-порты</b>. Эти порты — просто определённые адреса на шине ввода / вывода x86-системы. Операции чтения / записи на этих портах обрабатываются специальными инструкциями, встроенными в процессор.</p><h3>Чтение из портов и запись в них</h3><p>Доступ к портам I/O можно получить, используя инструкции in и out, являющиеся частью набора инструкций x86.</p><p>В read_port номер порта принимается как аргумент. Когда компилятор вызывает вашу функцию, он пушит все её аргументы в стек. Аргумент копируется в регистр edx по указателю на стек. Регистр dx — это младшие 16 бит edx. Инструкция in читает из порта, номер которого хранится в dx, и помещает результат в al. Регистр al — это младшие 8 бит eax. Если вы помните, чему вас учили, то знаете, что возвращаемые функциями значения передаются через регистр eax register. Таким образом, read_port обеспечивает чтение из I/O-портов.</p><p>write_port очень похожа. Мы принимаем два аргумента: номер порта и данные для записи. Инструкция out записывает данные в указанный порт.</p><h3>Прерывания</h3><p>Теперь, прежде чем мы продолжим писать какой-либо драйвер устройства, давайте разберемся, как процессор узнаёт о том, что устройство совершило действие.</p><p>Самое простое решение — это <a href="https://en.wikipedia.org/wiki/Polling_(computer_science)">поллинг</a>, постоянная проверка состояния устройства. По очевидным причинам это не самое практичное решение. Тут в дело вступают прерывания. Прерывание — это сигнал, посылаемый процессору аппаратным или системным обеспечением, который оповещает о событии. Используя прерывания, мы можем действовать только тогда, когда возникает интересующее нас прерывание.</p><p>Устройство, называемое <a href="https://ru.wikipedia.org/wiki/Контроллер_прерываний">контроллером прерываний</a> (Programmable Interrupt Controller, PIC), отвечает за обработку аппаратных прерываний и отправку их соответствующим системным прерываниям.</p><p>Когда на аппаратном обеспечении происходит какое-то событие, оно отправляет сигнал, называемый запросом прерывания, по свому специальному каналу на контроллер прерываний. Контроллер преобразует запрос в системное прерывание и отправляет его в процессор, где им занимается ядро.</p><p>Если бы у нас не было контроллера прерываний, то нам пришлось бы постоянно опрашивать все устройства на наличие произошедших событий.</p><p>Рассмотрим случай с клавиатурой. Она работает через I/O-порты 0x60 и 0x64. Порт 0x60 передаёт данные (о нажатой клавише), а порт 0x64 — состояние. Однако нужно знать наверняка, когда и из какого порта читать данные.</p><p>Для этого отлично подходят прерывания. Когда происходит нажатие клавиши, клавиатура посылает сигнал на контроллер прерываний по линии IRQ1. Контроллер обладает значением offset, полученным во время его инициализации. Он добавляет номер входной линии к этому offset и получает число прерывания. Затем процессор обращается к специальной структуре данных, называющейся дескрипторной таблицей прерываний (Interrupt Descriptor Table, IDT), для передачи обработчику прерываний адреса, соответствующего числу прерывания.</p><p>После этого запускается код, расположенный по этому адресу, и обрабатывает исключение.</p><h3>Настраиваем IDT</h3><p>Мы реализуем IDT как массив структур IDT_entry. Мы обсудим то, как прерывание клавиатуры привязано к своему обработчику, позже. Сперва посмотрим, как работают контроллеры прерываний.</p><p>Современные x86-системы имеют два контроллера прерываний по 8 входных линий. Назовём их PIC1 и PIC2. PIC1 получает сигналы с IRQ0 до IRQ7, а PIC2 — с IRQ8 до IRQ15. PIC1 использует порт 0x20 для команд и 0x21 — для данных. PIC2 использует порт 0xA0 для команд и 0xA1 — для данных.</p><p>Контроллеры инициализируются 8-битными инициализирующими командными словами (Initialization command words, ICW). Подробный синтаксис этих команд можно изучить <a href="http://stanislavs.org/helppc/8259.html">здесь</a>.</p><p>В защищённом режиме первой командой, которую вы должны передать двум контроллерам прерываний, является инициализирующая команда <b>ICW1</b> (0x11). Эта команда заставляет контроллер ждать поступления ещё трёх инициализирующих слов на порт данных.</p><p>Эти команды сообщают контроллерам следующее:</p><ul><li>сдвиг (ICW2);</li><li>состояние подключения (master/slave) (ICW3);</li><li>дополнительную информацию об окружении (ICW4).</li></ul><p>Вторая инициализирующая команда — это ICW2, которая записывает в порты данных каждого контроллера его сдвиг.</p><p>Контроллеры допускают каскадирование выводов и вводов, что настраивается командой ICW3, но мы не будем его использовать, поэтому установим все значения в ноль.</p><p>ICW4 устанавливает дополнительные параметры окружения. Мы  заполним младший бит, чтобы сказать контроллерам, что мы работаем в режиме 80×86.</p><p>Та-дам! Контроллеры прерываний инициализированы!</p><p>У каждого контроллера есть внутренний 8-битный регистр, называющийся регистр масок прерывания (Interrupt Mask Register, IMR). Этот регистр хранит битмэп линий IRQ, идущих в контроллер. Если бит установлен, контроллер игнорирует запрос. Это означает, что мы можем включать и выключать любую линию IRQ, меняя соответствующее значение в IMR. Чтение из порта данных возвращает значение из IMR, а запись — задаёт это значение. В нашем коде мы отключаем все линии IRQ, чтобы позже включить ту, что соответствует клавиатуре.</p><p>Если линии IRQ включены, наши контроллеры могут получать идущие по ним сигналы и преобразовывать их в числа прерываний, добавляя сдвиг. Теперь нам нужно найти IDT такую, чтобы число прерывания для клавиатуры было привязано к адресу обработчика событий клавиатуры, который мы напишем.</p><p>Какое число прерывания должно соответствовать обработчику событий клавиатуры?</p><p>Клавиатура использует IRQ<b>1</b>. Это линия ввода 1 контроллера PIC1. Его сдвиг равен 0x20. Для получения числа прерывания сложим 1 <b>+</b> 0x20 и получим 0x21. Таким образом, адрес обработчика событий должен быть связан с прерыванием 0x21 в IDT.</p><p>Теперь нам нужно определить IDT для прерывания 0x21. Мы привяжем это прерывание к функции keyboard_handler, которую запишем в ассемблерном файле.</p><p>Каждое значение в IDT состоит из 64 битов. В значении IDT для прерывания мы не храним адрес функции-обработчика целиком, а делим его на 2 части по 16 бит. Младшие биты хранятся в первых 16 битах значения IDT, а старшие 16 битов — в последних. Это сделано в целях совместимости с 286. Да, такие костыли от Intel можно встретить часто!</p><p>В значении IDT мы также должны задать тип — это нужно для того, чтобы поймать исключение. Нам также нужно передать коду ядра смещение. GRUB создаст для нас <a href="https://ru.wikipedia.org/wiki/GDT">глобальную таблицу дескрипторов</a>, каждое значение которое занимает 8 байт. Дескриптор кода ядра — это второй сегмент, поэтому его сдвиг равен 0x08. Окно прерывания равно 0x8e. Оставшиеся посередине 8 бит должны быть заполнены нулями. Таким образом, мы заполнили значение IDT в соответствии с прерыванием клавиатуры.</p><p>После того, как мы закончили все махинации с IDT, мы сообщаем процессору её адрес, используя инструкцию lidt, принимающую один операнд, который должен быть указателем на структуру дескрипторов, описывающую IDT.</p><p>Дескриптор достаточно прост. Он содержит размер IDT в байтах и её адрес. Для хранения значений я использовал массив, но вы можете воспользоваться структурой.</p><p>У нас есть указатель в переменной idt_ptr, который мы передаём в lidt, используя функцию load_idt().</p><p>Кроме того, функция load_idt() включает прерывания, используя инструкцию sti.</p><p>Когда IDT настроена, мы можем включить линию IRQ клавиатуры, используя маску прерываний, о которой мы говорили ранее.</p><h3>Функция обработки прерываний клавиатуры</h3><p>Итак, наши прерывания клавиатуры привязаны к функции keyboard_handler через значение IDT для прерывания 0x21. Каждый раз, когда мы нажимаем клавишу, мы можем быть уверены, что функция будет вызвана.</p><p>Эта функция просто вызывает другую функцию, написанную на Си, и возвращает значение, используя класс инструкций iret. Мы могли бы написать здесь весь необходимый код, но на Си писать безусловно приятнее. iret/iretd используются вместо ret при работе с прерываниями.</p><p>Сперва мы отправляем сигнал <b>EOI</b> (конец прерывания), записывая его в порт команд контроллера. Только после этого контроллер принимает следующие прерывания. Нам нужно читать два порта — порт данных 0x60 и порт команд / данных 0x64.</p><p>Сперва мы читаем порт 0x64 для получения состояния. Если младший бит состояния равен 0, то буфер пуст, и данных для чтения нет. В обратном случае мы читаем порт данных 0x60. Этот порт даёт нам код каждой нажатой клавиши. Мы используем простой массив символов в файле keyboard_map.h для связывания кодов и символов.</p><p>В этой статье мы обрабатываем только строчные буквы и цифры. Конечно, можно настроить и обработку других клавиш, а также их сочетаний.</p><p>Вы можете собрать ядро и запустить его на реальной машине или в эмуляторе (QEMU) так же, как и в прошлой статье.</p><p>Печатайте!</p><figure><img src="https://media.tproger.ru/uploads/2016/12/tumblr_inline_ncy1p0kSGj1rivrqc.png" alt="" /></figure>]]></content:encoded>
    </item>
    <item>
      <title>Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро</title>
      <link>https://tproger.ru/translations/lets-write-a-kernel-1</link>
      <comments>https://tproger.ru/translations/lets-write-a-kernel-1?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Иван Бирюков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/lets-write-a-kernel-1</guid>
      <description><![CDATA[<p>Arjun Sreedharan показывает, как написать ядро, загружаемое бутлоадером GRUB, и объясняет, что происходит с регистрами процессора при старте x86.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/lets-write-a-kernel-1">Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Операционные системы]]></category>
      <category><![CDATA[Язык Си]]></category>
      <category><![CDATA[Язык ассемблера]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Mon, 05 Dec 2016 18:50:15 GMT</pubDate>
      <content:encoded><![CDATA[<p>Рассказывает Arjun Sreedharan</p><p>Давайте напишем простое ядро, которое можно загрузить при помощи бутлоадера GRUB x86-системы. Это ядро будет отображать сообщение на экране и ждать.</p><figure><img src="https://media.tproger.ru/uploads/2016/12/one-does-not-kernel.jpg" alt="" /></figure><h3>Как загружается x86-система?</h3><p>Прежде чем мы начнём писать ядро, давайте разберёмся, как система загружается и передаёт управление ядру.</p><p>В большей части регистров процессора при запуске уже находятся определённые значения. Регистр, указывающий на адрес инструкций (Instruction Pointer, EIP), хранит в себе адрес памяти, по которому лежит исполняемая процессором инструкция. EIP по умолчанию равен <b>0xFFFFFFF0</b>. Таким образом, x86-процессоры на аппаратном уровне начинают работу с адреса 0xFFFFFFF0. На самом деле это — последние 16 байт 32-битного адресного пространства. Этот адрес называется вектором перезагрузки (reset vector).</p><p>Теперь карта памяти чипсета гарантирует, что 0xFFFFFFF0 принадлежит определённой части BIOS, не RAM. В это время BIOS копирует себя в RAM для более быстрого доступа. Адрес 0xFFFFFFF0 будет содержать лишь инструкцию перехода на адрес в памяти, где хранится копия BIOS.</p><p>Так начинается исполнение кода BIOS. Сперва BIOS ищет устройство, с которого можно загрузиться, в предустановленном порядке. Ищется магическое число, определяющее, является ли устройство загрузочным  (511-ый и 512-ый байты первого сектора должны равняться <b>0xAA55</b>).</p><p>Когда BIOS находит загрузочное устройство, она копирует содержимое первого сектора устройства в RAM, начиная с физического адреса <b>0x7c00</b>; затем переходит на адрес и исполняет загруженный код. Этот код называется бутлоадером.</p><p>Бутлоадер загружает ядро по физическому адресу <b>0x100000</b>. Этот адрес используется как стартовый во всех больших ядрах на x86-системах.</p><p>Все x86-процессоры начинают работу в простом 16-битном режиме, называющимся <b>реальным режимом</b>. Бутлоадер GRUB переключает режим в 32-битный <b>защищённый режим</b>, устанавливая нижний бит регистра CR0 в <b>1</b>. Таким образом, ядро загружается в 32-битном защищённом режиме.</p><p>Заметьте, что в случае с ядром Linux GRUB видит протоколы загрузки Linux и загружает ядро в реальном режиме. Ядро самостоятельно <a href="http://github.com/torvalds/linux/blob/master/arch/x86/boot/main.c#L181">переключается</a> в защищённый режим.</p><h3>Что нам нужно?</h3><ul><li>x86-компьютер;</li><li>Linux;</li><li><a href="http://www.nasm.us/">ассемблер NASM</a>;</li><li>gcc;</li><li>ld (GNU Linker);</li><li><a href="http://www.gnu.org/software/grub/">grub</a>;</li></ul><p>Исходники можно найти <a href="http://github.com/arjun024/mkernel">на GitHub</a>.</p><h3>Задаём точку входа на ассемблере</h3><p>Как бы не хотелось ограничиться одним Си, что-то придётся писать на ассемблере. Мы напишем на нём небольшой файл, который будет служить исходной точкой для нашего ядра. Всё, что он будет делать — вызывать внешнюю функцию, написанную на Си, и останавливать поток программы.</p><p>Как же нам сделать так, чтобы этот код обязательно был именно исходной точкой?</p><p>Мы будем использовать скрипт-линковщик, который соединяет объектные файлы для создания конечного исполняемого файла. В этом скрипте мы явно укажем, что хотим загрузить данные по адресу 0x100000.</p><p>Вот код на ассемблере:</p><p>Первая инструкция, bits 32, не является x86-ассемблерной инструкцией. Это директива ассемблеру NASM, задающая генерацию кода для процессора, работающего в 32-битном режиме. В нашем случае это не обязательно, но вообще полезно.</p><p>Со второй строки начинается секция с кодом.</p><p>global — это ещё одна директива NASM, делающая символы исходного кода глобальными. Таким образом, линковщик знает, где находится символ start — наша точка входа.</p><p>kmain — это функция, которая будет определена в файле kernel.c. extern значит, что функция объявлена где-то в другом месте.</p><p>Затем идёт функция start, вызывающая функцию kmain и останавливающая процессор инструкцией hlt. Именно поэтому мы заранее отключаем прерывания инструкцией cli.</p><p>В идеале нам нужно выделить немного памяти и указать на неё указателем стека (esp). Однако, похоже, что GRUB уже сделал это за нас. Тем не менее, вы всё равно выделим немного места в секции BSS и переместим на её начало указатель стека. Мы используем инструкцию resb, которая резервирует указанное число байт. Сразу перед вызовом kmain указатель стека (esp) устанавливается на нужное место инструкцией mov.</p><h3>Ядро на Си</h3><p>В kernel.asm мы совершили вызов функции kmain(). Таким образом, наш “сишный” код должен начать исполнение с kmain():</p><p>Всё, что сделает наше ядро — очистит экран и выведет строку “my first kernel”.</p><p>Сперва мы создаём указатель vidptr, который указывает на адрес <b>0xb8000</b>. С этого адреса в защищённом режиме начинается “видеопамять”. Для вывода текста на экран мы резервируем 25 строк по 80 ASCII-символов, начиная с 0xb8000.</p><p>Каждый символ отображается не привычными 8 битами, а 16. В первом байте хранится сам символ, а во втором — attribute-byte. Он описывает форматирование символа, например, его цвет.</p><p>Для вывода символа s зелёного цвета на чёрном фоне мы запишем этот символ в первый байт и значение 0x02 во второй. 0 означает чёрный фон, 2 — зелёный цвет текста.</p><p>Вот таблица цветов:</p><p>В нашем ядре мы будем использовать светло-серый текст на чёрном фоне, поэтому наш байт-атрибут будет иметь значение 0x07.</p><p>В первом цикле программа выводит пустой символ по всей зоне 80×25. Это очистит экран. В следующем цикле в “видеопамять” записываются символы из нуль-терминированной строки “my first kernel” с байтом-атрибутом, равным 0x07. Это выведет строку на экран.</p><h3>Связующая часть</h3><p>Мы должны собрать kernel.asm в объектный файл, используя NASM; затем при помощи GCC скомпилировать kernel.c в ещё один объектный файл. Затем их нужно присоединить к исполняемому загрузочному ядру.</p><p>Для этого мы будем использовать связывающий скрипт, который передаётся ld в качестве аргумента.</p><p>Сперва мы зададим формат вывода как 32-битный <a href="http://elinux.org/Executable_and_Linkable_Format_(ELF)">Executable and Linkable Format</a> (ELF). ELF — это стандарный формат бинарных файлов Unix-систем архитектуры x86. <b>ENTRY</b> принимает один аргумент, определяющий имя символа, являющегося точкой входа. <b>SECTIONS</b> — это самая важная часть. В ней определяется разметка нашего исполняемого файла. Мы определяем, как должны соединяться разные секции и где их разместить.</p><p>В скобках после SECTIONS точка (.) отображает счётчик положения, по умолчанию равный 0x0. Его можно изменить, что мы и делаем.</p><p>Смотрим на следующую строку: .text : { *(.text) }. Звёздочка (*) — это специальный символ, совпадающий с любым именем файла. Выражение *(.text) означает все секции .text из всех входных файлов.</p><p>Таким образом, линковщик соединяет все секции кода объектных файлов в одну секцию исполняемого файла по адресу в счётчике положения (0x100000). После этого значение счётчика станет равным 0x100000 + размер полученной секции.</p><p>Аналогично всё происходит и с другим секциями.</p><h3>Grub и Multiboot</h3><p>Теперь все файлы готовы к созданию ядра. Но остался ещё один шаг.</p><p>Существует стандарт загрузки x86-ядер с использованием бутлоадера, называющийся <b>Multiboot specification</b>. GRUB загрузит наше ядро, только если оно удовлетворяет <a href="http://www.gnu.org/software/grub/manual/multiboot/multiboot.html">этим спецификациям</a>.</p><p>Следуя им, ядро должно содержать заголовок в своих первых 8 килобайтах. Кроме того, этот заголовок должен содержать 3 поля, являющихся 4 байтами:</p><ul><li><b>магическое</b> поле: содержит магическое число <b>0x1BADB002</b> для идентификации ядра.</li><li>поле <b>flags</b>: нам оно не нужно, установим в ноль.</li><li>поле <b>checksum</b>: если сложить его с предыдущими двумя, должен получиться ноль.</li></ul><p>Наш kernel.asm станет таким:</p><h3>Строим ядро</h3><p>Теперь мы создадим объектные файлы из kernel.asm и kernel.c и свяжем их, используя наш скрипт.</p><p>Эта строка запустит ассемблер для создания объектного файла kasm.o в формате ELF-32.</p><p>Опция “-c” гарантирует, что после компиляции не произойдёт скрытого линкования.</p><p>Это запустит линковщик с нашим скриптом и создаст исполняемый файл, называющийся <b>kernel</b>.</p><h3>Настраиваем grub и запускаем ядро</h3><p>GRUB требует, чтобы имя ядра удовлетворяло шаблону kernel-&lt;version&gt;. Поэтому переименуйте ядро. Своё я назвал kernel-701.</p><p>Теперь поместите его в директорию <b>/boot</b>. Для этого понадобятся права суперпользователя.</p><p>В конфигурационном файле GRUB grub.cfg добавьте следующее:</p><p>Не забудьте убрать директиву hiddenmenu, если она есть.</p><p>Перезагрузите компьютер, и вы увидите список ядер с вашим в том числе. Выберите его, и вы увидите:</p><figure><img src="https://media.tproger.ru/uploads/2016/12/mkernel.png" alt="" /></figure><p>Это ваше ядро! В <a href="https://tproger.ru/translations/lets-write-a-kernel-1/">следующей части</a> добавим систему ввода / вывода.</p><h3>P.S.</h3><ul><li>Для любых фокусов с ядром лучше использовать виртуальную машину.</li><li>Для запуска ядра в <b>grub2 </b>конфиг должен выглядеть так:menuentry 'kernel 7001' {tset root='hd0,msdos1'tmultiboot /boot/kernel-7001 ro}</li><li>если вы хотите использовать эмулятор qemu, используйте:qemu-system-i386 -kernel kernel</li></ul>]]></content:encoded>
    </item>
    <item>
      <title>"Гарантия корректности" как методика обхода багов</title>
      <link>https://tproger.ru/translations/proof-of-correctness-method</link>
      <comments>https://tproger.ru/translations/proof-of-correctness-method?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Лапа]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/proof-of-correctness-method</guid>
      <description><![CDATA[<p>Доказать корректность программы целиком удаётся не всегда, поэтому Саймон Пейтон-Джонс предлагает заранее обозначать, что в ней должно и не должно случиться.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/proof-of-correctness-method">"Гарантия корректности" как методика обхода багов</a>»</p>]]></description>
      <category><![CDATA[Алгоритмы и структуры данных]]></category>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Лучшая практика]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Sat, 29 Oct 2016 19:01:20 GMT</pubDate>
      <content:encoded><![CDATA[<p>Вам когда-нибудь приходилось <i>гарантировать корректность выполнения</i> своей программы? На самом деле это большой объем работы, который не всегда возможно выполнить. В самих доказательствах могут быть ошибки, об этом говорится и в мудрости, взятой из <a href="http://www.mit.edu/~xela/tao.html">Дао программирования</a>:</p><blockquote>Даже в идеальных программах есть ошибки.</blockquote><p>Столкнувшись с этими трудностями, <a href="https://ru.wikipedia.org/wiki/Пейтон-Джонс,_Саймон">Саймон Пейтон-Джонс</a> предложил вот такой выход:</p><p>“Я думаю, будет довольно продуктивно сразу обозначить, что в программе обязательно должно или не должно произойти. Например, “это дерево всегда должно быть сбалансировано”. Или “эта функция всегда должна возвращать число больше нуля”. Может быть, в будущем найдется более простой способ гарантировать правильную работу всей программы, но пока что его нет”.</p><p>По сути, мы до сих пор не можем гарантировать отсутствие ошибок. Хорошие программисты всегда спрашивают себя, как и почему работает каждый участок кода. Они сразу же пытаются воспроизвести ситуацию, где что-то идет не так. Предугадать все получается не всегда — и когда что-то все же идет не так, программист сразу же пытается придумать способ больше не допускать этой ошибки.</p><p>Это основной тезис практических доказательств. И кстати: хорошие программисты сразу пытаются увериться в том, что все будет всегда работать как надо, но в одном они порой промахиваются. Они не делятся своими рассуждениями с другими. А ведь доказательство должно пройти проверку не только его создателем — важно проверить, верно ли оно, пересказав свои мысли кому-то еще.</p><p>Кстати, вы уже могли слышать этот тезис: некоторые программисты ставят на стол фигурку, игрушку — любой предмет, с которым можно обсудить проблему. Именно в ходе объяснения проблемы кому-то другому мы чаще всего и видим все выходы и недочеты. Поэтому не бойтесь выглядеть глупо и не стесняйтесь обсуждать с кем-либо свои идеи.</p><p>Вот вам пример ситуации, когда необходимо быть уверенным хоть в чем-то: многопоточность. Это сложно и трудно предсказуемо — нельзя покрыть юнит-тестами весь код, слишком много всего зависит от случайностей. Поэтому для того, чтобы снизить вероятность ошибок, нужно спроектировать все так, чтобы у вас было точное логическое обоснование того, что ошибка не может возникнуть.</p><p>Вот этот код может уверенно заявить о себе: здесь не могут возникнуть условия гонки (race conditions) и дедлоки. Обратите внимание на то, как именно программист решил эту проблему:</p><p>Об условиях Коффмана можно почитать <a href="http://www.personal.kent.edu/~rmuhamma/OpSystems/Myos/deadlockCondition.htm">здесь</a>.</p><p>Можно сделать и проще. Например, как говорит <a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D0%BC%D0%BF%D1%81%D0%BE%D0%BD,_%D0%9A%D0%B5%D0%BD">Кен Томпсон</a>: “У моего кода есть одна отличительная черта: он простой, отрывистый и короткий. Так его может прочитать любой человек”.</p><p>Так что этот пример на самом деле можно улучшить: например, автоматизировать разблокировки с помощью макроса, использовать язык или библиотеку, поддерживающие более высокоуровневое управление потоками.</p><p>В этом примере программист решил, что для работы его кода важно, чтобы в каждый момент времени был открыт только один клапан. Вся логика находится в одном месте, а для будущих программистов предусмотрены комментарии, объясняющие, почему все сделано именно так.</p><p>Конечно, и этот пример не совершенен. Что будет делать hardwareOpen() при возникновении ошибки? Понимание того, какие вызовы происходят в каждом из случаев, обязательно при написании кода, гарантирующего отсутствие определенных ошибок.</p><p>Каскадный код — место, в котором нужно быть особенно внимательным. Также будьте предельно осторожны при работе с полученными от пользователя значениями: они могут обрушить всю программу вне зависимости от используемого языка, а обычным тестированием выловить связанные с вводом ошибки очень сложно.</p><p>Иначе говоря, начните думать о том, как сделать ваш код безопаснее и обеспечить правильную работу его частей — так вы уменьшите количество багов… и сделаете шаг на пути к дзену, конечно же.</p><p><a href="https://github.com/ZeroBugsAndProgramFaster/melons">Немного исходников из этой статьи</a> (примеры написаны на M4, довольно редком языке).</p>]]></content:encoded>
    </item>
    <item>
      <title>Викторина: проведет ли компилятор GCC эти оптимизации?</title>
      <link>https://tproger.ru/quiz/gcc-optimization</link>
      <comments>https://tproger.ru/quiz/gcc-optimization?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Иван Бирюков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/quiz/gcc-optimization</guid>
      <description><![CDATA[<p>Шесть вопросов с парами фрагментов кода до и после преобразования: нужно решить, сделает ли так GCC 4.2.1 и корректна ли сама предложенная оптимизация.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/quiz/gcc-optimization">Викторина: проведет ли компилятор GCC эти оптимизации?</a>»</p>]]></description>
      <category><![CDATA[C++]]></category>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Оптимизация]]></category>
      <category><![CDATA[Викторины]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Sat, 22 Oct 2016 13:19:36 GMT</pubDate>
      <content:encoded><![CDATA[<p>Посмотрим, как хорошо вы знаете способности GCC оптимизировать код. Эта викторина содержит 6 вопросов, в каждой по две вставки кода. Первая вставка иллюстрирует код до некой оптимизации, вторая — после нее. Сможет ли GCC изменить первый код таким образом, чтобы он стал вторым? Верна ли вообще проведенная оптимизация (быстрее ли второй код первого)?</p><p>Для тестов использовался древний GCC 4.2.1. Если новые версии ведут себя по-другому, то обязательно сообщите об этом нам! Будьте осторожны: не все предложенные оптимизации корректны.</p><p>Итак, на каждый вопрос есть три варианта ответа: «GCC сделает это», «GCC не может так сделать», «Оптимизация неверна». Постарайтесь отвечать на вопросы самостоятельно.</p>]]></content:encoded>
    </item>
    <item>
      <title>Советы по языку программирования Си: 10 полезных приемов</title>
      <link>https://tproger.ru/translations/10-useful-things-in-c</link>
      <comments>https://tproger.ru/translations/10-useful-things-in-c?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Иван Бирюков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/10-useful-things-in-c</guid>
      <description><![CDATA[<p>Приёмы для начинающих и опытных Си-разработчиков — от указателей на функции до других возможностей языка, где легко выстрелить себе в ногу.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/10-useful-things-in-c">Советы по языку программирования Си: 10 полезных приемов</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Советы]]></category>
      <category><![CDATA[Язык Си]]></category>
      <category><![CDATA[Кодстайл]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Fri, 30 Sep 2016 21:02:39 GMT</pubDate>
      <content:encoded><![CDATA[<p>Си — это один из самых важных и широко распространённых языков программирования. Его можно использовать не только для общих целей, но и для написания низкоуровневых программ, работающих с “железом”. Си позволяет программисту многое из того, чего не позволяют другие языки. Однако в этом кроется как сильная, так и слабая сторона языка: можно писать высокопроизводительный код, но гораздо проще выстрелить себе в ногу. Поэтому мы делимся с вами десятью советами, которые пригодятся как начинающим, так и опытным Си-разработчикам.</p><h3>1. Указатели на функцию</h3><p>Иногда бывает удобно хранить функцию в переменной. Этот способ нечасто используется в повседневном программировании, но его можно использовать для улучшения модульности программы или для обработки события.</p><p>Этот приём заключается в следующем. Сперва нужно задать тип “указатель на функцию, возвращающую что-то” и использовать его для объявления переменной. Рассмотрим простой пример. Сначала я задаю тип PFC (Pointer to a Function returning a Character):</p><p>Затем использую его для объявления переменной z:</p><p>Определяю функцию a():</p><p>Адрес функции теперь хранится в z:</p><p>Заметим, что вам не нужен оператор &amp; ("address-of"); компилятор знает, что a должна быть адресом функции. Так происходит из-за того, что с функцией можно произвести лишь две операции: 1) вызвать её или 2) взять её адрес. Поскольку вызова функции не происходит (отсутствуют скобки), остаётся лишь вариант с получением адреса, который помещается в z.</p><p>Чтобы вызвать функцию, адрес которой находится в z, просто добавьте скобки:</p><h3>2. Списки аргументов переменной длины</h3><p>Обычно вы объявляете функцию, которая принимает фиксированное число аргументов. Тем не менее, можно написать функцию, которая принимает любое их количество. Стандартная функция printf() тому доказательство. Разумеется, вы можете сами написать подобную функцию. Вот пример:</p><p>Первый аргумент, arg_count — это целое, в котором хранится реальное число аргументов, следующих за ним в списке переменных, указанном как три точки.</p><p>С переменными аргументами работают несколько встроенных функций и макросов: va_list, va_start, va_arg и va_end (они определены в stdarg.h).</p><p>Сперва вам нужно объявить указатель на список аргументов:</p><p>Затем установите argp в первый аргумент переменной части. Это первый аргумент после последнего фиксированного (в нашем случае arg_count):</p><p>Теперь извлекаем каждую переменную по очереди, используя va_arg:</p><p>Заметим, что вам нужно знать тип аргумента заранее (в нашем случае int) и число аргументов (у нас задаётся фиксированным arg_count).</p><p>Наконец, нужно убраться при помощи va_end:</p><h3>3. Проверка и установка отдельных битов</h3><p><a href="https://tproger.ru/links/bitwise-tricks/">Битовые операции</a> иногда воспринимаются как некий сорт тёмной магии, используемой продвинутыми программистами. Да, работа с битами напрямую может быть весьма непонятной, но понимание этого процесса может вам пригодиться.</p><p>Обсудим, зачем это вообще нужно. Программы часто используют переменные-флаги для хранения булевых величин. У вас в коде вполне могут встречаться такие переменные:</p><p>Если они как-то связаны, как в примере выше (они все описывают состояние движения), то зачастую бывает удобнее хранить их в одной “переменной состояния” и использовать каждый бит для задания того или иного состояния:</p><p>Тогда вы сможете использовать битовые операции для задания или обнуления отдельных битов:</p><p>Преимуществом является то, что вся информация хранится в одном месте, и очевидно, что вы работаете с одной логической сущностью.</p><p>В архиве с кодом, который будет дан в конце статьи, есть пример работы с битами.</p><p>Чтобы установить заданный бит переменной value (в диапазоне от 0 до 31), используйте такое выражение:</p><p>Для очистки бита используйте:</p><p>А для получения значения бита:</p><h3>4. Ленивые логические операторы</h3><p>Логические операторы Си, &amp;&amp; (“и”) и || (“или”), позволяют вам составлять цепочки условий в тех случаях, когда действие должно выполняться при выполнении всех условий (&amp;&amp;) или только одного (||). Но в Си также есть операторы &amp; и |. Очень важно понимать разницу между ними. Если вкратце, двухсимвольные операторы (&amp;&amp; и ||) называются “ленивыми” операторами. Если они используются между двумя выражениями, то второе будет выполнено, только если первое оказалось верным, иначе оно пропускается. Рассмотрим пример:</p><p>Тест (int)f &amp;&amp; feof(f) должен вернуть истинный результат, когда будет достигнут конец файла f. Сперва тест вычисляет f; она будет равна нулю (ложное значение), если файл не был открыт. Это ошибка, поэтому попытка чтения файла не увенчается успехом. Однако, поскольку первая часть теста провалена, вторая не будет обработана, и feof() не будет запущена. Этот пример демонстрирует корректное использование ленивого оператора. Теперь посмотрите на этот код:</p><p>В этом случае используется оператор &amp;, а не &amp;&amp;. Оператор &amp; — это инструкция для выполнения обоих выражений при любых условиях. Поэтому, даже если первая часть теста провалится, вторая будет выполнена. Это может привести к различным ошибкам.</p><h3>5. Тернарные операторы</h3><p>Операция называется тернарной. когда принимает три операнда. В Си тернарный оператор ? : можно использовать для сокращённой записи тестов if..else. Общий синтаксис выглядит так:</p><p>Пусть у нас есть две целых переменных, t and items. Мы можем использовать if..else для проверки значения items и присваивания её значения переменной t таким образом:</p><p>Используя тернарный оператор, эту запись можно сократить до одной строки:</p><figure><img src="https://media.tproger.ru/uploads/2016/09/2-3.png" alt="" /></figure><p>Если вы не привыкли к тернарным операторам, то они могут показаться вам странными, но на самом деле они весьма удобны.</p><p>Рассмотрим ещё один пример. Этот код выводит первую строку, когда у нас один предмет, и вторую, когда их несколько:</p><p>Это можно переписать так:</p><h3>6. Стек</h3><p><a href="https://ru.wikipedia.org/wiki/Стек">Стек</a> — это LIFO-хранилище. Вы можете использовать адресную арифметику для добавления элементов в стек или извлечения их из него. Часто под стеком подразумевается структура, используемая Си для хранения локальных переменных функции. Но на самом деле стек — это общий тип структуры данных, которым вы спокойно можете пользоваться.</p><p>Код ниже задаёт очень маленький стек: массив _stack из двух целых. Помните, что при тестировании всегда лучше использовать небольшие числа. Если код содержит ошибки, найти их при работе с массивом из 2 элементов будет проще, чем если их будет 100. Также объявляется указатель на стек _sp и устанавливается в основание стека _stack:</p><p>Теперь определим функцию push(), которая помещает целое в стек. Она возвращает новое число элементов в стеке или -1, если стек полон:</p><p>Для получения элементов стека нужна функция pop(). Она возвращает новое число элементов в стеке или -1, если он пуст:</p><p>А вот пример, демонстрирующий работу со стеком:</p><h3>7. Копирование данных</h3><p>Вот три способа копирования данных. Первый использует стандартную функцию memcpy(), которая копирует n байт из src в dst:</p><p>Теперь посмотрим на самодельную альтернативу memcpy(). Она может быть полезной, если копируемые данные нужно как-то обработать:</p><p>И наконец, функция, использующая 32-битные целые для ускорения копирования. Помните, что скорость в конечном итоге зависит от оптимизации компилятора. В этом примере предполагается, что счётчик данных n кратен 4 из-за работы с 4-байтовыми указателями:</p><p>Примеры можно найти в архиве ниже.</p><h3>8. Использование заголовочных файлов</h3><p>Си использует заголовочные файлы (.h), которые могут содержать объявления функций или констант. Заголовочный файл можно импортировать в код двумя способами: если файл предоставляется компилятором, используйте #include &lt;string.h&gt;, а если файл написан вами — #include "mystring.h". В сложных программах есть риск того, что вы можете подключить один и тот же заголовочный файл несколько раз.</p><figure><img src="https://media.tproger.ru/uploads/2016/09/3-3.png" alt="" /></figure><p>Предположим, что у нас есть простой заголовочный файл, header1.h, содержащий следующие определения:</p><p>Затем создадим другой файл header2.h, содержащий это:</p><p>Добавим в нашу программу, main.c, это:</p><p>При компиляции программы мы получим ошибку компиляции, потому что T_SIZE будет объявлена дважды (её определение в header1 подключено к двум разным файлам). Мы должны подключить header1 к header2 для того, чтобы header2 компилировался в тех случаях, когда header1 не используется. И как это исправить? Можно написать макрос для header1:</p><p>Эта проблема настолько распространена, что многие IDE делают это за вас. Тем не менее, если вы будете делать это вручную, делайте это осторожно.</p><h3>9. Скобки: нужны ли они?</h3><p>Вот несколько простых правил:</p><ol><li>Скобки нужно использовать для изменения порядка выполнения операторов. Например, 3 * (4 + 3) — не то же самое, что 3 * 4 + 3 .</li><li>Скобки можно использовать для улучшения читаемости. Здесь они, очевидно, не нужны:t = items &gt; 0 ? items : -items;Приоритет оператора || ниже, чем &lt; и &gt;. Однако, в этом случае скобки точно не будут лишними:(x &gt; 0) || (x &lt; 100 &amp; y &gt; 10) || (y &lt; 0)</li><li>Скобки стоит использовать в макросах:#define MYCONST (4 + 3)Вы не знаете, где будете использовать эту константу, поэтому скобки нужны. Рассмотрим такой случай применения:3 * MYCONSTБез скобок результат получился бы неверным из-за порядка выполнения операторов.</li></ol><p>Но в одном месте скобки точно не нужны: в выражении после return. Например, это…</p><p>…выполнится так же, как это:</p><h3>10. Массивы как адреса</h3><p>Программисты, которые учат Си после какого-то другого языка, часто удивляются, когда Си работает с массивами как с адресами и наоборот. Массив — это контейнер фиксированного размера, а адрес — это число, связанное с местом в памяти; разве они связаны?</p><p>Выходит, что да.</p><p>Си прав: массив — это просто адрес базы в блоке памяти, а форма записи массива, например, в Java или JavaScript — просто синтаксический сахар.</p><p>Присмотритесь к этому коду:</p><p>Первый цикл здесь копирует адрес каждого элемента массива в сам массив:</p><p>На каждой итерации адрес увеличивается на i. Поэтому адрес переменной _x будет первым элементом, а каждый следующий адрес — адресом  _x плюс 1. Когда мы прибавляем 1 к адресу массива, компилятор Си вычисляет подходящий сдвиг в зависимости от типа данных (в нашем случае 4 байта для массива целых).</p><p>Второй цикл выводит значения, хранящиеся в массиве, сперва выводя адрес элемента _x + i, затем значение элемента через привычный вид массива _x[i], а потом содержимое массива с использованием адресной нотации (где оператор * возвращает содержимое памяти по адресу в скобках): *(_x + i). Во всех случаях значения будут одинаковыми. Это наглядно демонстрирует, что массив и адрес — это одно и то же.</p><p>Обратите внимание, что для получения адреса массива вам не нужен оператор &amp;, поскольку компилятор считает, что массив и есть адрес.</p><p>Для того, чтобы попробовать применить эти советы на практике, вы можете <a href="https://web.archive.org/web/20160629110159/https://dujk9xa5fr1wz.cloudfront.net/extra_html/mix/c_tips.zip">скачать</a> исходники.</p>]]></content:encoded>
    </item>
    <item>
      <title>В Сети появилась серия статей-руководств по созданию собственной ОС на ассемблере и Rust</title>
      <link>https://tproger.ru/articles/os-creation-series</link>
      <comments>https://tproger.ru/articles/os-creation-series?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Иван Бирюков]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/os-creation-series</guid>
      <description><![CDATA[<p>В блоге Филиппа Опперманна разобрана разработка 64-битной операционной системы: первые части на ассемблере, дальше Rust. Исходники доступны на GitHub.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/os-creation-series">В Сети появилась серия статей-руководств по созданию собственной ОС на ассемблере и Rust</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Операционные системы]]></category>
      <category><![CDATA[Обучение программированию]]></category>
      <category><![CDATA[Rust]]></category>
      <category><![CDATA[Язык ассемблера]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Tue, 23 Aug 2016 18:33:23 GMT</pubDate>
      <content:encoded><![CDATA[<p>Предлагаем вашему вниманию серию англоязычных статей, опубликованную в <a href="http://os.phil-opp.com">блоге Филиппа Опперманна.</a> В ней подробно разбирается процесс разработки собственной 64-битной ОС на ассемблере и <a href="https://www.rust-lang.org/en-US/">Rust</a>. Серия пополняется, но на основе уже написанных статей можно написать достаточно функциональную операционную систему.</p><p>Разработка ведётся на Linux, весь исходный код доступен на <a href="https://github.com/phil-opp/blog_os">GitHub</a>. В первых двух статьях автор использует ассемблер для написания ядра, после этого — Rust.</p><p>Статьи написаны простым, понятным даже начинающим программистам языком, все термины либо объясняются в статье, либо снабжены ссылками на другие Интернет-ресурсы.</p><p>Реализация этого проекта однозначно будет вам полезна: во-первых, вы сможете освоить или подтянуть знания по используемым в нём языкам программирования, а во-вторых, разберётесь в основных принципах работы операционной системы.</p><p>Итак, вот список статей:</p><ol><li><a href="http://os.phil-opp.com/multiboot-kernel.html">Пишем простое x86-ядро</a>.</li><li><a href="http://os.phil-opp.com/entering-longmode.html">Переход в 64-битный режим</a>.</li><li><a href="http://os.phil-opp.com/set-up-rust.html">Настройка Rust</a>.</li><li><a href="http://os.phil-opp.com/printing-to-screen.html">Настройка интерфейса вывода</a>.</li><li><a href="http://os.phil-opp.com/allocating-frames.html">Аллокатор фреймов</a>.</li><li><a href="http://os.phil-opp.com/modifying-page-tables.html">Таблицы страниц</a>.</li><li><a href="http://os.phil-opp.com/remap-the-kernel.html">«Ремап» ядра</a>.</li><li><a href="http://os.phil-opp.com/kernel-heap.html">Куча.</a></li><li><a href="http://os.phil-opp.com/catching-exceptions.html">Обработка исключений</a>.</li><li><a href="http://os.phil-opp.com/better-exception-messages.html">Сообщения об исключениях</a>.</li><li><a href="http://os.phil-opp.com/returning-from-exceptions.html">Возврат из исключений</a>.</li><li><a href="http://os.phil-opp.com/double-faults.html">Двойные ошибки (ошибки исключений)</a>.</li></ol><p>Серия продолжает пополняться, поэтому периодически сюда заглядывайте, мы будем дополнять вышеуказанный список.</p>]]></content:encoded>
    </item>
    <item>
      <title>О битовых операциях</title>
      <link>https://tproger.ru/translations/bitwise-operations</link>
      <comments>https://tproger.ru/translations/bitwise-operations?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Типичный программист]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/bitwise-operations</guid>
      <description><![CDATA[<p>Побитовые операторы работают напрямую с битами числа: двоичные примеры, таблицы истинности и связь с логическими вентилями цифровой техники.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/bitwise-operations">О битовых операциях</a>»</p>]]></description>
      <category><![CDATA[Для начинающих]]></category>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 10 Mar 2016 21:37:31 GMT</pubDate>
      <content:encoded><![CDATA[<p>В этой статье я расскажу вам о том, как работают битовые операции. С первого взгляда они могут показаться вам чем-то сложным и бесполезным, но на самом деле это совсем не так. В этом я и попытаюсь вас убедить.</p><h3>Введение</h3><p>Побитовые операторы проводят операции непосредственно на битах числа, поэтому числа в примерах будут в двоичной системе счисления.</p><p>Я расскажу о следующих побитовых операторах:</p><ul><li>|  (Побитовое ИЛИ (OR)),</li><li>&amp; (Побитовое И (AND)),</li><li>^ (Исключающее ИЛИ (XOR)),</li><li>~ (Побитовое отрицание (NOT)),</li><li>&lt;&lt; (Побитовый сдвиг влево),</li><li>&gt;&gt; (Побитовый сдвиг вправо).</li></ul><p>Битовые операции изучаются в дискретной математике, а также лежат в основе цифровой техники, так как на них основана логика работы логических вентилей — базовых элементов цифровых схем. В дискретной математике, как и в цифровой технике, для описания их работы используются таблицы истинности. Таблицы истинности, как мне кажется, значительно облегчают понимание битовых операций, поэтому я приведу их в этой статье. Их, тем не менее, почти не используют в объяснениях побитовых операторов высокоуровневых языков программирования.</p><p>О битовых операторах вам также необходимо знать:</p><ol><li>Некоторые побитовые операторы похожи на операторы, с которыми вы наверняка знакомы (&amp;&amp;, ||). Это потому, что они на самом деле в чем-то похожи. Тем не менее, путать их ни в коем случае нельзя.</li><li>Большинство битовых операций являются операциями составного присваивания.</li></ol><h3>Побитовое ИЛИ (OR)</h3><p>Побитовое ИЛИ действует эквивалентно логическому ИЛИ, но примененному к каждой паре битов двоичного числа. Двоичный разряд результата равен 0 только тогда, когда оба соответствующих бита в равны 0. Во всех других случаях двоичный результат равен 1. То есть, если у нас есть следующая таблица истинности:</p><p>38 | 53 будет таким:</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-21/f81c7d76-245f-4bfe-93cf-a06a28bab4e3.png" alt="" /></figure><p>В итоге мы получаем 1101112 , или 5510 .</p><h3>Побитовое И (AND)</h3><p>Побитовое И — это что-то вроде операции, противоположной побитовому ИЛИ. Двоичный разряд результата равен 1 только тогда, когда оба соответствующих бита операндов равны 1. Другими словами, можно сказать, двоичные разряды получившегося числа — это результат умножения соответствующих битов операнда: 1х1 = 1, 1х0 = 0. Побитовому И соответствует следующая таблица истинности:</p><p>Пример работы побитового И на выражении 38 &amp; 53:</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-21/e397e257-528d-4354-a1f1-e6acf7f7f779.png" alt="" /></figure><p>Как результат, получаем 1001002 , или 3610 .</p><p>С помощью побитового оператора И можно проверить, является ли число четным или нечетным. Для целых чисел, если младший бит равен 1, то число нечетное (основываясь на преобразовании двоичных чисел в десятичные). Зачем это нужно, если можно просто использовать %2? На моем компьютере, например, &amp;1 выполняется на 66% быстрее. Довольно неплохое повышение производительности, скажу я вам.</p><h3>Исключающее ИЛИ (XOR)</h3><p>Разница между исключающим ИЛИ и побитовым ИЛИ в том, что для получения 1 только один бит в паре может быть 1:</p><p>Например, выражение 138^43 будет равно…</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-21/72ca2fec-961f-407a-bb71-170843376cd8.png" alt="" /></figure><p>… 101000012 , или 16010</p><p>С помощью ^ можно поменять значения двух переменных (имеющих одинаковый тип данных) без использования временной переменной.</p><p>Также с помощью исключающего ИЛИ можно зашифровать текст. Для этого нужно лишь итерировать через все символы, и ^ их с символом-ключом. Для более сложного шифра можно использовать строку символов:</p><p>Исключающее ИЛИ не самый надежный способ шифровки, но его можно сделать частью шифровального алгоритма.</p><h3>Побитовое отрицание (NOT)</h3><p>Побитовое отрицание инвертирует все биты операнда. То есть, то что было 1 станет 0, и наоборот.</p><p>Вот, например, операция ~52:</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-21/96c2bab1-baf1-48a9-9192-c23b597961c1.png" alt="" /></figure><p>Результатом будет 20310</p><p>При использовании побитового отрицания знак результата всегда будет противоположен знаку исходного числа (при работе со знаковыми числами). Почему так происходит, узнаете прямо сейчас.</p><h3>Дополнительный код</h3><p>Здесь мне стоит рассказать вам немного о способе представления отрицательных целых чисел в ЭВМ, а именно о дополнительном коде (two’s complement). Не вдаваясь в подробности, он нужен для облегчения арифметики двоичных чисел.</p><p>Главное, что вам нужно знать о числах, записанных в дополнительном коде — это то, что старший разряд является знаковым. Если он равен 0, то число положительное и совпадает с представлением этого числа в прямом коде, а если 1 — то оно отрицательное. То есть, 10111101 — отрицательное число, а 01000011 — положительное.</p><p>Чтобы преобразовать отрицательное число в дополнительный код, нужно инвертировать все биты числа (то есть, по сути, использовать побитовое отрицание) и добавить к результату 1.</p><p>Например, если мы имеем 109:</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-21/b4765f7c-7517-4ecb-a4ab-431e463134de.png" alt="" /></figure><p>Представленным выше методом мы получаем -109 в дополнительном коде.<br />Только что было представлено очень упрощенное объяснение дополнительного кода, и я настоятельно советую вам детальнее изучить эту тему.</p><h3>Побитовый сдвиг влево</h3><p>Побитовые сдвиги немного отличаются от рассмотренных ранее битовых операций. Побитовый сдвиг влево сдвигает биты своего операнда на N количество битов влево, начиная с младшего бита. Пустые места после сдвига заполняются нулями. Происходит это так:</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-21/5e1e156c-556d-44a8-8513-c3b49d1434cd.png" alt="" /></figure><p>Интересной особенностью сдвига влево на N позиций является то, что это эквивалентно умножению числа на 2N. Таким образом, 43&lt;&lt;4 == 43*Math.pow(2,4). Использование сдвига влево вместо Math.pow обеспечит неплохой прирост производительности.</p><h3>Побитовый сдвиг вправо</h3><p>Как вы могли догадаться, &gt;&gt; сдвигает биты операнда на обозначенное количество битов вправо.</p><p>Если операнд положительный, то пустые места заполняются нулями. Если же изначально мы работаем с отрицательным числом, то все пустые места слева заполняются единицами. Это делается для сохранения знака в соответствии с дополнительным кодом, объясненным ранее.</p><p>Так как побитовый сдвиг вправо — это операция, противоположная побитовому сдвигу влево, несложно догадаться, что сдвиг числа вправо на N количество позиций также делит это число на 2N. Опять же, это выполняется намного быстрее обычного деления.</p><h3>Вывод</h3><p>Итак, теперь вы знаете больше о битовых операциях и не боитесь их. Могу предположить, что вы не будете использовать &gt;&gt;1 при каждом делении на 2. Тем не менее, битовые операции неплохо иметь в своем арсенале, и теперь вы сможете воспользоваться ими в случае надобности или же ответить на каверзный вопрос на собеседовании.</p>]]></content:encoded>
    </item>
    <item>
      <title>Самоизменяющаяся программа на C под x86_64</title>
      <link>https://tproger.ru/translations/writing-a-self-mutating-program</link>
      <comments>https://tproger.ru/translations/writing-a-self-mutating-program?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Типичный программист]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/translations/writing-a-self-mutating-program</guid>
      <description><![CDATA[<p>Программа, меняющая собственный код во время выполнения, сложна в отладке и платформозависима, но остаётся хорошим учебным примером на C и ассемблере.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/translations/writing-a-self-mutating-program">Самоизменяющаяся программа на C под x86_64</a>»</p>]]></description>
      <category><![CDATA[Низкоуровневое программирование]]></category>
      <category><![CDATA[Язык Си]]></category>
      <category><![CDATA[Переводы]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Mon, 09 Feb 2015 16:30:59 GMT</pubDate>
      <content:encoded><![CDATA[<blockquote>Зачем вообще может понадобиться писать программу, которая меняет свой код во время выполнения? Это ужасно!</blockquote><p>Да, да, я знаю. И все-таки, зачем? Ну, например, это хороший учебный пример. Но главная причина — потому что могу.</p><p>Самоизменяющиеся программы сами по себе не очень полезны. Они сложны в отладке, платформо-зависимы, код ужасно нечитаем, если вы, конечно, не эксперт в ассемблере. Единственное хорошее применение им — скрытие вредоносного кода. Но мой интерес чисто академический, поэтому ничего такого тут не будет.</p><blockquote>Дисклеймер: в этой статье очень много ассемблерного кода под x86_64, в котором я не эксперт. Многому я научился в процессе написания статьи, поэтому возможно (почти наверняка) в тексте есть ошибки. Если вы найдете ошибку, пришлите мне е-мейл, чтобы я мог ее исправить.</blockquote><p>Первый шаг к написанию самоизменяющейся программы — получение возможности изменять код во время выполнения. Когда-то давно программисты поняли, что это плохая идея и с тех пор добавили защиту на изменение выполняемого кода. Для начала разберемся, где хранятся инструкции, когда программа выполняется. Перед выполнением загрузчик помещает всю программу целиком в память. Затем программа запускается в виртуальном адресном пространстве, управляемом ядром ОС. Адресное пространство разбито на различные области, как показано на рисунке.</p><figure><img src="https://media.tproger.ru/uploads/2015/02/segments.jpg" alt="" /></figure><p>В нашем случае, нас интересует только область инструкций (text segment). Именно туда загружаются команды процессора. Адресное пространство размечено на страницы, которыми и управляет ядро. Страницы соответствуют физической памяти компьютера. Ядро также управляет правами доступа к страницам. По умолчанию область инструкций можно читать и выполнять оттуда команды. Писать в нее нельзя. Для того, чтобы изменять инструкции в этой области памяти, мы должны изменить права доступа.</p><p>Изменить права доступа можно функцией mprotect() (<a href="http://man7.org/linux/man-pages/man2/mprotect.2.html">man mprotect</a>). Единственная сложность в том, что указатель, передаваемый в функцию, должен быть выровнен по началу страницы памяти. Вот функция, которая по данному ей указателю находит адрес начала страницы и устанавливает права на чтение, запись и выполнение на ней.</p><p>Теперь, если мы передадим функции указатель, она сделает страницу памяти с этим адресом перезаписываемой. Важно заметить, что некоторые ОС могут отказаться устанавливать права доступа на запись в область инструкций. Я использую Linux, который позволяет сделать это. Если вы используете другую систему, убедитесь, что проверяете значение, возвращенное mprotect(). В примера дальше по тексту предполагается, что код, который мы будем менять, находится целиком на одной странице памяти. Для длинных участков кода это может не сработать.</p><p>Итак, мы можем писать в область инструкций. Следующий вопрос — что мы будет туда записывать?</p><p>Давайте начнем с простого. Допусти, у меня есть такая функция:</p><p>foo() создает локальную переменную i и присваивает ей значение 0, затем инкрементирует это значение и выводит в stdout. Посмотрим, сможем ли мы поменять число, которое прибавляется к i.</p><p>Для того, чтобы это сделать, нам надо посмотреть не только инструкции, которые сгенерирует компилятор для foo(), но и машинный код. Давайте напишем полную программу с использованием foo():</p><p>Теперь, когда у нас есть программа, использующая foo(), мы можем ее скомпилировать.</p><p>И вот тут начинается самое интересное. Нам надо дизассемблировать бинарник, созданный gcc и посмотреть, какие инструкции соответствуют foo(). Мы можем это сделать с помощью утилиты objdump.</p><p>Если вы откроете файл foo.dis в текстовом редакторе, примерно на 128 строке (в зависимости от версии компилятора, foo() может быть собрана с помощью разных инструкций) вы увидите дизассемблированную функцию foo(). Выглядит она примерно так:</p><p>Если вы раньше не сталкивались с ассемблерным кодом под x86_64, вы можете потеряться. Вот что здесь происходит: мы смещаем стек на 4 байта (размер int на моей машине) для того, чтобы освободить место для переменной i. Затем инициализируем эти 4 байта нулями и прибавляем 1 к этому значению. Все, что после адреса 40054b — это подготовка к вызову функции printf().</p><p>Итак, если мы хотим изменить число, прибавляемое к i, надо поменять вот эту инструкцию:</p><p>Прежде чем продолжать, давайте посмотрим, из чего состоит инструкция.</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-19/0215527b-33cd-4fb5-abf0-006e2d653246.png" alt="" /></figure><p>Рассмотрим саму инструкцию и ее операнды:</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-19/39643daf-3c4a-4f66-b97a-7dd0c84f059d.png" alt="" /></figure><p>Теперь, когда мы разобрали человекочитаемую форму записи инструкции, давайте посмотрим на машинный код. Все инструкции для x86_64 имеют следующий формат:</p><figure><img src="https://media.tproger.ru/uploads/2015/02/Instruction-Format.jpg" alt="" /></figure><p>Здесь возникают сложности. Дело в том, что инструкции для x86_64 могут иметь переменную длину, их разбор вручную сложен и занимает много времени. Задачу облегчают различные источники документации. На <a href="http://ref.x86asm.net/">x86ref.net</a> есть отличная документация. Для смелых есть также документация Intel в виде <a href="http://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-manual-325462.html">PDF</a> на 3000 страниц.</p><p>В нашем случае эти байты означают следующее:</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-19/fae80699-34f9-47c0-a180-723b2a716119.png" alt="" /></figure><p>В документации есть полезная таблица, в которой объясняется значение каждого ModR/M байта. Также эта таблица есть в руководстве, упомянутом выше, таблица 2-2 в разделе 2-5 тома 2A (455 страница PDF-файла).</p><p>Теперь мы знаем какой байт в какой инструкции небходимо поменять, осталось понять, как это сделать.</p><p>Итак, мы хотим поменять байт 01 в инструкции addl $0x1,-0x4(%rbp).</p><p>Для этого нам нужно знать адрес этого байта. Мы можем без затруднений найти адрес начала функции, так что нам нужно знать только смещение. Есть два способа это сделать:</p><ol><li>Посмотреть на дизассемблированный код и посчитать число байт между началом функции и интересующим байтом.</li><li>Написать функцию, которая последовательно напечатает инструкции foo() со смещением относительно начала функции</li></ol><p>Почему бы не сделать и то, и другое?</p><p>Посмотрим сначала на дизассемблированную функцию:</p><p>Функция начинается с адреса 400538, а байт, который нас интересует — на 400550 (400547 + 3), следовательно, смещение будет равно 400550 – 400538 = 18.</p><p>Можно убедиться в этом, написав короткую процедуру, которая выводит на экран инструкции для данной функции. Вот слегка измененная программа:</p><p>Заметьте, что для того, чтобы определить длину функции foo(), я добавил пустую функцию bar() после foo(). Теперь, когда сразу после foo() расположена bar(), мы можем определить длину foo() вычитанием адреса bar() из адреса foo().</p><p>Вот что выводит эта программа:</p><p>Наш байт 0x1 расположен по адресу 0x40057e. Можем убедиться, что смещение действительно 18.</p><blockquote>Примечание переводчика: На текущей версии gcc этот код требует для компиляции специальных опций. Поскольку в редакции использовалась система, отличная от той, что у автора статьи, мы приводим код, который работает у нас: http://ideone.com/Ufvzba</blockquote><p>Теперь все готово для изменения кода во время выполнения. Имея указатель на foo() мы можем получить указатель типа unsigned char на байт, которым хотим поменять.</p><p>Если мы все сделали правильно, эта строка поменяет аргумент команды addl на 0x2A или 42. Теперь, когда мы вызовем foo(), она напечатает 42 вместо 1.</p><p>Теперь соберем все вместе:</p><p>Скомпилируем это:</p><p>И запустим:</p><p>Ура! Первый вызов foo() выводит 1, как и указано в коде. Затем, после того, как мы его изменили, она выводит 42.</p><p>Теперь у нас есть самоизменяющаяся программа! Однако, она довольно скучная. Все что мы сделали — поменяли 1 на 42. Было бы круто, если бы мы смогли заставить foo() делать что-то совсем другое. Например вызывать шелл код с помощью <a href="http://www.opennet.ru/cgi-bin/opennet/man.cgi?topic=exec">exec()</a>.</p><p>Итак, как мы можем заставить foo() вызывать шелл код? Самый простой способ — использовать системный вызов <a href="http://www.opennet.ru/man.shtml?category=2&amp;russian=&amp;topic=execve">execve</a>. Но это уже сложнее, чем просто замена байта.</p><p>Если мы хотим подменить foo() системным вызовом, нам потребуются код для этого вызова. К счастью для нас, специалисты по безопасности любят использовать машинный код для запуска шелла, так что мы може этим воспользоваться. Поиск по «x86_64 shellcode» дал вот такой результат:</p><p>Я взял этот код <a href="http://www.exploit-db.com/exploits/13691/">отсюда</a> и немного изменил:</p><ul><li>Я добавил xor %rax, %rax для того, чтобы обнулить %rax. Иначе он мог вызвать сегфолт.</li><li>Я заменил значение $0x68732f6e69622f2f на $0x68732f6e69622f00. Таким образом я избавился от сдвига и сохранил размер кода в пределах 30 байт. Обычно шелл код вроде этого инжектируется через переполнение буфера или другими способами атаки. Строки в C заканчиваются нулевым байтом. Поэтому большинство функций для работы со строками из стандартной библиотеки возвращают результат когда встречают нулевой байт. Специалисты по безопасности избегают использования нуль-терминатора по этой причине, но в нашем случае в этом нет ничего страшного, поэтому мы заменяем 0x2f на 0x00 и избавляемся от команды сдвига. См. оригинал кода по ссылке.</li></ul><p>Прежде чем продолжать, давайте разберемся, как работает вышеприведенный код. Сначала рассмотрим системный вызов. Системный вызов — это вызов функции ядра ОС с запросом о каком-либо действии. Это может быть что-то, на что есть права доступа только у ядра, поэтому мы к нему и обращаемся. В нашем случае системный вызов execve говорит ядру, что мы хотим запустить другой процесс и заменить адресное пространства нашего процесса адресным пространством нового. Это значит, что при успешном вызове execve наш процесс завершится.</p><p>Для того, чтобы вызвать системную функцию на x86_64, нам необходимо его подготовить, поместив в правильные регистры определенные значения, а затем, собственно, вызвать системную функцию. Значения и регистры зависят от конкретной системы. Я пишу под Linux, поэтому привожу документацию для execve в Linux.</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-19/15b27fce-354c-49ef-bb54-c55db5c116b9.png" alt="" /></figure><p>Важно помнить, что в регистрах должны быть указатели на реальные значения в памяти. Это значит, что мы должны положить корректные значения в стек, а затем поместить их адреса в стеке в регистры. Самое время сказать, что скучаешь по простоте указателей в C.</p><p>Полный список системных вызовов можно найти <a href="http://blog.rchapman.org/post/36801038863/linux-system-call-table-for-x86-64">здесь</a>.</p><p>Если вы знакомы с прототипом execve() в C (я привел его ниже для справки), вы заметите, насколько системный вызов в C похож на вызов функции.</p><p>Стоит также заметить, что вызов системной процедуры отличается в x86 и x86_64. Отдельной инструкции для системного вызова на x86 вообще нет, вместо этого он производится вызовом прерывания. Более того, на Linux, номер системного вызова execve разный для x86 и x86_64. (11 на x86; 59 на x86_64).</p><p>Теперь, когда мы знаем, как вызвать системную функцию, рассмотрим подробно каждый шаг нашего кода:</p><figure><img src="https://media.tproger.ru/user-uploads/33794/2024-02-19/f8ae81f6-848a-4766-b912-659e84392e45.png" alt="" /></figure><p>Ух! Тут много всего. Но зато теперь мы можем заменить foo() на вызов шелла.</p><p>Теперь, вместо того, чтобы изменить один байт в теле функции foo(), нам надо поменять ее полностью. Это можно сделать с помощью memcpy(). Имея указатель на начало foo() и указатель на шелл код, мы можем скопировать шелл код на место foo() так:</p><p>Нужно быть осторожным, чтобы не зайти за пределы foo(). В нашем случае foo() занимает 41 байт, а шелл код — 29 байт, так что все в порядке. Заметьте, что наш шелл код это строка, и в ее конце стоит нулевой байт. Так как мы хотим скопировать только код, то мы вычитаем единицу из sizeof(shellcode) когда передаем аргумент в memcpy()</p><p>Отлично! Теперь соберем все вместе:</p><p>И скомпилируем:</p><p>Пора скрестить пальцы и запустить.</p><p>Вот! Теперь у нас есть самоизменяющаяся программа на C.</p><p>Перевод статьи «Writing a Self-Mutating x86_64 C Program»</p>]]></content:encoded>
    </item>
  </channel>
</rss>