ПроКодинг - Откроем для вас мир IT!

Любая фото, песня, переписка и даже перевод денег - это цепочки из 0 и 1. Компьютеры говорят на бинарном языке. Кстати, в вопросе подвох: не буква I, а цифра 1. И да, это единственный язык, который железо понимает напрямую.

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

Основа - бит (0 или 1). Восемь бит - байт. Полезно держать в голове опорные числа: 2^10 = 1024 (≈1 КиБ), 2^20 ≈ 1 МиБ, 2^30 ≈ 1 ГиБ. Это быстро объясняет, откуда берутся «странные» 1024 вместо 1000.

Интересный факт: были попытки уйти от двоичной логики. В 1958 в МГУ сделали троичный компьютер «Сетунь» на сбалансированном троичном коде. Он работал, но индустрия выбрала двоичную электронику - проще компоненты, лучше помехоустойчивость и инструменты.

Биты сами по себе - просто числа. Чтобы показать буквы и эмодзи, нужны кодировки. Классика: ASCII - буква A это десятичное 65, в двоичном 01000001. Русские буквы живут в Unicode. В UTF-8 заглавная Я (U+042F) - это два байта: D0 AF, в двоичном 11010000 10101111. Один и тот же текст в разных кодировках - разные байты.

Хочется увидеть биты у себя? Быстро и без магии: на Linux/macOS - xxd -b файл.bin (покажет биты) или hexdump -C файл.bin (шестнадцатеричный дамп с ASCII). На Windows - PowerShell: Format-Hex .\файл.bin. Для строки - echo "A" | xxd -b и любуйтесь 01000001.

Как же из битов складывается «ум»? На уровне железа всё строится на логических воротах: AND, OR, NOT, XOR. Из ворот - сумматоры, регистры, АЛУ, кэш, процессор. А уже поверх - компиляторы и языки. Вы пишете на Python/Go, компилятор и ОС превращают это в машинный код - те самые 0 и 1.

Немного практики для повседневной пользы. Права Unix - это биты: rwx для владельца, группы и всех. Команда chmod 644 - это 110 100 100: владелец читает и пишет, остальные только читают. Флаги в конфигурациях - тоже биты: 1 (0001), 2 (0010), 4 (0100), 8 (1000). Комбинируете их через OR, проверяете через AND - быстро и экономно.

Что значит язык 0 и 1

Под «языком 0 и 1» обычно понимают бинарный язык - способ представить любые данные и команды как последовательность двух стабильных состояний. Это не буква «I», а цифра «1». Два состояния удобны железу: их проще различать в схеме и удерживать без ошибок.

На физическом уровне «0» и «1» - не конкретные числа, а диапазоны напряжений. Например, в классической 5‑вольтовой TTL‑логике вход меньше или равен 0,8 В считается нулём, а больше или равен 2,0 В - единицей. В современных CMOS‑семействах пороги обычно задают как долю от питания (около 0,3·Vdd для «0» и 0,7·Vdd для «1»). Конкретные цифры всегда смотрите в даташите микросхемы.

Логика / питаниеПорог «0» (VIL, макс.)Порог «1» (VIH, мин.)Примечание
TTL 5 В (74LS)≤ 0,8 В≥ 2,0 ВНечётные пороги, совместимость с широким семейством TTL
CMOS 3,3 В≈ 0,3·Vdd (≤ 0,99 В)≈ 0,7·Vdd (≥ 2,31 В)Типичные значения для 3,3 В CMOS
CMOS 1,8 В≈ 0,3·Vdd (≤ 0,54 В)≈ 0,7·Vdd (≥ 1,26 В)Типичные значения для 1,8 В CMOS

Логическая «1» и «0» дальше проходят через логические ворота (AND, OR, NOT, XOR) и регистры. Из этих кирпичиков строятся сумматоры, кэш, контроллеры, а в итоге - процессор. На другом конце - память и шины, которые переносят те же биты.

Бит - минимальная единица информации. Из 8 бит получается байт, а из n бит можно представить 2^n различных состояний. Поэтому 8 бит дают 256 вариантов: достаточно, чтобы закодировать символ из ASCII, небольшой цвет палитры или часть машинной команды.

Числа в процессоре хранятся не «как в тетради», а по соглашениям. Для со знаком почти везде используется двоичное дополнение (two’s complement): самый старший бит - знак, а отрицательные числа считаются как 2^n − |x|. Это даёт одну арифметику для сложения и вычитания без дополнительных правил.

Текст - это тоже числа. Буква A в ASCII - десятичное 65, в шестнадцатеричном 0x41, в двоичном 01000001. Для мировых языков используют Unicode. В UTF‑8 один и тот же символ занимает разное число байт: латиница - 1 байт, кириллица - чаще 2, эмодзи - 4. Например, «Я» - два байта: 0xD0 0xAF.

Команды процессора - машинный код, то есть такие же байты. Архитектура (x86‑64, ARM, RISC‑V) описывает, как байты раскладываются на опкод и операнды. Примеры для x86‑64: NOP - 0x90; MOV EAX, 1 - байты B8 01 00 00 00; ADD EAX, EBX - 01 D8 (в двоичном это 00000001 11011000). Эти байты идут по шине, попадают в кэш инструкций, декодируются и исполняются.

  1. Процессор выбирает (fetch) следующую команду по адресу из IP/RIP.
  2. Декодирует (decode) байты: что за операция, где операнды.
  3. Исполняет (execute): АЛУ делает вычисления, логика управляет переходами.
  4. Записывает результаты (writeback) в регистры или память, двигает счётчик команд.

Исторический факт: ранний ENIAC работал на десятичной логике (кольцевые счётчики), но индустрия быстро перешла на двоичную электронику - проще схемы, выше помехоустойчивость, легче масштабирование.

Как быстро «читать» двоичное? Удобно держать в голове связь «одна шестнадцатеричная цифра = 4 бита». Тогда 0x90 - это 1001 0000, 0xAF - 1010 1111. Большие массивы байт обычно смотрят в hex, потому что он короче в 4 раза и легко переводится в биты.

  • Для констант используйте префиксы: 0b1010 (двоичное), 0xFF (шестнадцатеричное), 42 (десятичное).
  • Работайте с битовыми масками: OR (|) включает биты, AND (&) проверяет, XOR (^) переключает, NOT (~) инвертирует.
  • За порогами логических уровней всегда идите в даташит: параметры VIL/VIH и тайминги спасают от «призрачных» багов железа.

Вывод простой: «язык 0 и 1» - это общий слой, где сходятся устройства, кодировки и инструкции. Всё остальное - способы удобно собрать и разобрать эти биты.

Биты, байты и кодировки

Бит - это минимальная единица данных: 0 или 1. 8 бит - байт. Полбайта (4 бита) - «ниббл». В современных системах байт почти всегда 8 бит, и это уже де-факто стандарт.

Память и диски любят степени двойки: 2^10 = 1024, 2^20 = 1048576, 2^30 ≈ 1,07 млрд. Отсюда и двоичные приставки: 1 KiB = 1024 B, 1 MiB = 1024 KiB. Десятичные - это 1 KB = 1000 B, 1 MB = 1000000 B. В утилитах можно встретить оба варианта, так что хорошо понимать разницу.

Шестнадцатеричная запись удобна, потому что 1 hex-цифра покрывает ровно 4 бита. Например, байт 0xAF - это биты 1010 1111. Поэтому дампы показывают байты в hex, а не в длинных цепочках из нулей и единиц.

То, что мы зовём бинарный язык, - это условные «слова» из битов. Для чисел важны знаковость и ширина. Без знака 8 бит дают диапазон 0…255, со знаком -128…127 (дополнительный код). 32-битное со знаком - примерно −2,147,483,648…2,147,483,647. 64-битное - около ±9,22×10^18. Эти границы влияют на переполнения и сериализацию данных.

Порядок байтов (endianness) относится к многобайтовым числам: x86 - little-endian, сетевые протоколы - big-endian (network byte order). Для UTF‑8 порядок байтов не важен - это поток байтов, читаемый слева направо. А вот для UTF‑16/32 порядок важен; его обозначают BOM или настройками протокола.

ASCII - это 7-битная таблица на 128 символов (U+0000…U+007F). Русские буквы, иероглифы и эмодзи там не помещаются. Unicode описывает весь набор символов (сейчас более 149 тысяч), до U+10FFFF (17 «плоскостей»). На практике на вебе доминирует UTF‑8 - более 97% сайтов используют его (по W3Techs), и это отличный дефолт для файлов и API.

UTF‑8 - это переменная длина: символ занимает 1-4 байта. Базовый ASCII кодируется одним байтом, кириллица - обычно двумя, «евро» - тремя, эмодзи - чаще четырьмя. Ниже - наглядная таблица с примерами и точными диапазонами.

Длина (байт)Диапазон кодпоинтовПрефикс битовПримерUTF‑8 байты (hex)
1U+0000-U+007F0xxxxxxx'A' (U+0041)41
2U+0080-U+07FF110xxxxx 10xxxxxx'Я' (U+042F)D0 AF
3U+0800-U+FFFF1110xxxx 10xxxxxx 10xxxxxx'€' (U+20AC)E2 82 AC
4U+10000-U+10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx'🙂' (U+1F642)F0 9F 99 82

Пара точных примеров в битах. 'A' - десятичное 65, hex 0x41, биты 01000001. 'Я' - кодпоинт U+042F, в UTF‑8 два байта: D0 AF, биты 11010000 10101111. Всё сходится с шаблонами из таблицы выше.

Про «рассинхрон» между байтами и «символами». В UTF‑8 длина строки в байтах почти всегда больше числа видимых знаков: одна кириллическая буква - 2 байта, большинство эмодзи - 4 байта. К тому же «символ на экране» может быть не одним кодпоинтом: флаг 🇺🇦 - это два кода (региональные индикаторы), буква с диакритикой может быть «буква + комбинирующий знак». Для поиска и сравнения помогает нормализация Unicode (NFC/NFD).

BOM: в UTF‑8 это три байта EF BB BF в начале файла. Он не обязателен и иногда мешает (скрипты с shebang, JSON в некоторых парсерах). Для кроссплатформенных текстов безопаснее сохранять UTF‑8 без BOM.

Про однобайтные «кодировки» вроде Windows‑1251. Они фиксируют 256 позиций и конфликтуют между собой. Если видите кракозябры, почти всегда виноват неверно угаданная кодировка. Универсальный совет: хранить в UTF‑8, явно указывать encoding в заголовках HTTP и в открытии файлов (напр., в Python: encoding='utf-8').

Короткие практические подсказки:

  • Считайте в байтах, когда речь про лимиты API и дисковое место. «100 символов» в базе - не равно «100 байт».
  • Для двоичных флагов используйте маски 1, 2, 4, 8… и операции OR/AND - это компактно и быстро.
  • В протоколах фиксируйте endianness для чисел и кодировку для текста прямо в спецификации.
Как железо хранит нули и единицы

Как железо хранит нули и единицы

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

Что считается «нулём» и «единицей» на проводе? В цифровых схемах задают уровни. В классических TTL-входах «0» - до 0.8 В, «1» - от 2.0 В (при питании 5 В). В современном CMOS пороги зависят от питания (ядро CPU может работать около 0.7-1.2 В), но идея та же: ниже порога - «0», выше - «1». Для «грязных» сигналов ставят входы со Шмитт-триггером, чтобы отсечь шум и дрожание фронтов.

Дальше - носители состояния. Их два больших семейства: volatile (всё пропадает без питания) и non‑volatile (питание сняли - биты сохранились). Волатильная память нужна для скорости (регистры, кэши, ОЗУ), неволетильная - для долгого хранения (SSD, HDD, ПЗУ).

SRAM - это 6 транзисторов, скрещённых в две «защелкнутые» инверторные пары. Пока есть питание, ячейка удерживает «0» или «1». Очень быстро (наносекунды), но дорого по площади, поэтому из SRAM делают кэши (L1/L2/L3) и регистровые файлы.

DRAM хранит бит в конденсаторе (1T1C): заряд есть - «1», стекло - «0». Заряд утекает, поэтому строки памяти обновляют каждые ~64 мс (чаще при высоких температурах). Мизерный сигнал считывают усилители-сенсоры, которые одновременно «переписывают» заряд назад. Типичные задержки доступа - десятки наносекунд. Отсюда знакомые тайминги вроде tRCD, CL, tRP. Известная уязвимость Rowhammer как раз бьёт по этим механикам - частыми чтениями вызывает наводки и случайные флипы в соседних строках.

Флеш-память (NAND) использует «плавающий затвор»: на нём накапливается заряд, который сдвигает порог транзистора. SLC хранит 1 бит на ячейку (2 уровня), MLC - 2 бита (4 уровня), TLC - 3 (8 уровней), QLC - 4 (16 уровней). Больше уровней - выше плотность, но ниже ресурс и скорость. Примерные циклы записи/стирания: SLC 50-100 тыс., MLC ~3 тыс., TLC 1-3 тыс., QLC ~200-1 тыс. Поэтому контроллеры делают выравнивание износа (wear leveling) и сильную коррекцию ошибок (LDPC/BCH).

Жёсткие диски записывают биты как направление намагниченности доменов на дорожке. Современная перпендикулярная запись даёт плотности свыше 1 Тбит/дюйм², а новые технологии (HAMR/MAMR) поднимают потолок ещё выше. Время доступа ограничено механикой: перемещение головки и ожидание сектора под ней - это миллисекунды. Для надёжности на каждом секторе есть свой ECC и CRC; у современных дисков размер сектора - 4096 байт (4Kn) или эмуляция 512e.

Оптика (CD/DVD/BD) читает разницу отражения «ямок» и «площадок». Это тоже два состояния, но физически - про свет. Задержки выше, носители чувствительны к царапинам, зато дешёво архивировать и передавать.

НосительКак кодируется битВолатильностьТипичные задержкиРесурс/особенности
SRAM (кэш)Стабильное состояние защёлкнутых инверторовДа~0.5-2 нс (L1), единицы-десятки нс (L2/L3)Быстро и дорого по площади
DRAM (ОЗУ)Заряд конденсатора + sense-усилителиДа (refresh ~64 мс)~50-100 нс до строки; полная память ~70-100 нс+Дёшево и ёмко; чувствительно к помехам (Rowhammer)
NAND Flash (SSD)Заряд на плавающем затворе (уровни порога)НетЧтение ~50-100 мкс; запись ~0.5-3 мс; erase ~2-10 мсВыравнивание износа; SLC>MLC>TLC>QLC по ресурсу
HDDНаправление намагниченности доменовНетПоиск/вращение ~5-15 мсДёшево за ГБ; механика и вибрации влияют на ошибки
Оптические дискиОтражение от «ямок» и «площадок»Нет~100 мс и вышеДёшево хранить, чувствительно к царапинам/сроку

Передача нулей и единиц по плате и кабелям тоже не всегда «одним проводом и напряжением». На скоростях PCIe/USB/DisplayPort используют дифференциальные пары и кодирование (раньше 8b/10b, сейчас в PCIe 3.0+ - 128b/130b), чтобы лучше синхронизироваться и ловить ошибки.

Ошибки случаются. Космические частицы могут флипнуть бит в DRAM (soft error). ECC-память с кодами типа SECDED ловит и исправляет одиночные ошибки. В большом исследовании Google (2009) около 8% модулей DRAM в дата-центрах за год ловили исправляемые ошибки - это нормальная статистика боевой эксплуатации. В SSD контроллер постоянно считает и исправляет биты по LDPC, а уровень необратимых ошибок (UBER) держат в районе 10⁻¹⁵-10⁻¹⁷.

Немного практики, чтобы держать биты в порядке:

  • Проверьте, есть ли у вас ECC: в Linux - dmidecode -t memory, в Windows - wmic MemoryChip get DataWidth,TotalWidth (если TotalWidth больше DataWidth - есть ECC).
  • Смотрите износ SSD: smartctl -a /dev/nvme0 (SATA - атрибуты Wear_Leveling_Count/Media_Wearout_Indicator; NVMe - Percentage Used).
  • Оставляйте SSD 10-20% свободного места - контроллеру легче распределять записи и меньше износ.
  • На HDD держите резерв под SMART-переназначение секторов, следите за Reallocated_Sector_Ct и Pending_Sector.
  • Тепло ускоряет деградацию. Хороший обдув продлевает жизнь и DRAM, и NAND.

Если коротко: «0» и «1» - это просто договорённость поверх конкретной физики. Мы выбираем стабильные состояния, задаём пороги, фильтруем шум и прикручиваем исправление ошибок. Дальше - дело схем и протоколов, которые быстро и предсказуемо тасуют эти состояния между регистрами, памятью и дисками.

От логических ворот к языкам высокого уровня

Внизу всё начинается с логических ворот: AND, OR, NOT, XOR. Из двух NAND можно собрать NOT, а из NAND и OR - что угодно. Это не теория для учебника - NAND и NOR функционально полны, этого хватает, чтобы построить любой логический блок. Дальше - триггеры (SR, D), из них - регистры, счётчики и память. К этому добавляем АЛУ - устройство, которое умеет складывать, вычитать, сдвигать и сравнивать биты. Так из ворот получаются «кирпичи» процессора. Такт подаёт ритм (сейчас частоты обычно 2-5 ГГц), и за каждый тик данные проходят кусочек пути по конвейеру.

Правила, на каком наборе бит что делать, описывает ISA - архитектура набора инструкций. Примеры: x86-64 (Intel/AMD, корни в 8086 1978 года), ARMv8-A (мобильные и серверы), RISC‑V (свободная архитектура из Berkeley, спецификация открыта). Инструкция - это опкод плюс операнды и режим адресации. Для наглядности: в x86-64 «add eax, ebx» превращается в байты 01 D8 (опкод 0x01 и байт ModR/M 0xD8). Эти два байта для процессора - команда «сложи содержимое EBX к EAX».

ISA - это «договор» между софтом и железом. Как именно чип исполняет команды - дело микроархитектуры. Современные x86-64 разбирают сложные инструкции на микро‑операции, выполняют их вне очереди, предсказывают ветвления и держат кэш инструкций/данных (обычно L1/L2/L3). Микрокод можно обновлять - Linux и Windows грузят патчи микрокода для процессоров Intel и AMD, чтобы чинить уязвимости и баги без замены железа. Всё это не меняет ISA: собранный файл продолжает работать.

Чтобы людям не писать байты вручную, есть ассемблер - удобные мнемоники и синтаксис. Ассемблер превращает текст в объектные файлы (ELF в Linux, PE/COFF в Windows) с таблицами символов и релокациями. Линкер склеивает объектники и библиотеки, раскладывает код и данные по сегментам, правит адреса. Динамическая линковка подключает .so/.dll во время запуска через загрузчик (ld-linux.so, ntdll/loader). Посмотреть, что получилось, можно так:

  • Linux/macOS: objdump -d -M intel ./prog, readelf -a ./prog, nm ./prog
  • Windows (MSVC/PowerShell): dumpbin /DISASM prog.exe, link /DUMP /ALL prog.obj
  • Сразу из исходников: gcc -O3 -S file.c (даст .s), clang -S -emit-llvm file.c (IR)

Компилятор мостит путь от кода на C/Go/Rust к байтам. Конвейер такой: парсер строит AST, затем преобразует в промежуточное представление (IR), чаще в SSA‑форме. Дальше идут оптимизации (inline, удаление мёртвого кода, распространение констант, векторизация), распределение регистров, выбор инструкций под конкретную ISA и уже потом - генерация машинных инструкций и линковка. LLVM - популярный стек (Clang/LLVM IR/LLD). GCC имеет свой GIMPLE/RTL. Да, опции важны: -O0 - быстрые сборки для отладки, -O3 - агрессивная оптимизация, -march=native - использовать возможности конкретного CPU.

Функции между собой договариваются через ABI и соглашения о вызовах. На x86-64 в Linux/BSD (System V AMD64) первые шесть целочисленных аргументов идут в rdi, rsi, rdx, rcx, r8, r9, результат - в rax, стек 16-байтно выровнен. В Windows x64 - rcx, rdx, r8, r9. Пролог/эпилог функции готовит стековый фрейм, сохраняет нужные регистры и возвращает управление через ret. Эти детали важны для FFI, написания обёрток и чтения дампов стека.

Не весь код заранее компилируется в нативные инструкции. Есть байткоды и JIT. CPython исполняет байткод .pyc интерпретатором. JVM берёт .class, профилирует и JIT’ит горячие участки (HotSpot C2). .NET Core использует RyuJIT. V8 в JavaScript сначала интерпретирует (Ignition), потом оптимизирует (Turbofan). Обратная сторона - паузы и прогрев. Альтернатива - AOT: Rust и Go сразу в нативный код, Java может через GraalVM Native Image, .NET - NativeAOT. Есть и переносимый «низкоуровневый» формат - WebAssembly: компактные инструкции, безопасная песочница, исполняется в браузерах и не только.

Чуть про память и биты, которые всплывают в «высоком» коде. Большинство настольных и серверных систем - little‑endian (x86-64 таков; ARM умеет оба, но в Windows/Android/Linux обычно little‑endian). Страница памяти обычно 4 КиБ, виртуальная память изолирует процессы, а MMU мапит страницы на физику. В C/C++ есть строгая алиасинг‑модель и «неопределённое поведение»: например, переполнение знакового int - UB, и оптимизатор вправе удалять «невозможные» ветви. Отсюда редкие, но злые баги.

Как всё это применить на практике уже сегодня:

  1. Соберите функцию с разными оптимизациями и посмотрите ассемблер: gcc -O0/-O3 -S file.c. Обратите внимание, как меняются циклы и работают векторы (инструкции AVX2/AVX‑512).
  2. Разберите байты инструкции: echo -ne "\x01\xD8" | ndisasm -b32 - покажет «add eax, ebx».
  3. Поймайте реальные вызовы: perf top (Linux) или Windows Performance Analyzer - увидите горячие функции и их символы.
  4. Поиграйте с Compiler Explorer (godbolt.org): справа ассемблер, меняйте флаги и код, следите, как меняются инструкции.

И да, всё это по итогу сводится к одному: исходник превращается в машинный код, тот в процессоре разлетается по конвейеру, а ворота щёлкают такты за тактом. Разница «высокий-низкий» - это удобство для человека. Для железа это всегда нули и единицы.

Практика: смотрим и трогаем биты

Практика: смотрим и трогаем биты

Разберёмся без теории на пальцах: как увидеть байты любого файла, чем отличаются выводы на разных ОС, как проверить кодировку текста, что такое «магические» сигнатуры, и как аккуратно менять отдельные биты.

Начнём с просмотра байтов. На Linux и macOS есть hexdump и часто xxd (ставится вместе с Vim). В Windows это делает PowerShell командой Format-Hex.

  1. Создайте маленький файл без перевода строки (чтобы не мешал байт 0A):

    printf 'AЯ€' > sample.txt     # macOS/Linux, UTF-8 по умолчанию
    # или точно те же байты:
    printf '\x41\xD0\xAF\xE2\x82\xAC' > sample.bin
  2. Посмотрите шестнадцатеричный и двоичный дамп:

    # macOS/Linux
    hexdump -C sample.txt
    xxd -b sample.txt
    
    # Windows PowerShell (v5+)
    Format-Hex .\sample.txt

    Факт: hexdump -C и Format-Hex по умолчанию показывают по 16 байт в строке. xxd -b выводит биты для каждого байта, слева - смещение (offset) в файле.

  3. Проверьте, как кодируются символы:

    # Посчитать байты (не символы)
    wc -c < sample.txt          # Linux/macOS
    (Get-Item .\sample.txt).Length  # Windows

    В UTF-8: «A» - 1 байт (0x41), «Я» - 2 байта (0xD0 0xAF), «€» - 3 байта (0xE2 0x82 0xAC). Это видно в дампе.

Если вместо printf вы сделали echo, не забудьте флаг -n. Иначе попадёт перевод строки (LF, байт 0x0A). В Windows программы иногда добавляют CRLF (0x0D 0x0A). Это не «мусор», а часть текста. Просто учитывайте.

Хотите увидеть «магические» сигнатуры файлов? Они стоят в начале и помогают системам понять тип без расширения.

ФорматПервые байты (hex)Комментарий
PNG89 50 4E 47 0D 0A 1A 0AПодпись PNG фиксирована, 8 байт
JPEGFF D8 FFНачало файла (SOI + маркер)
ZIP50 4B 03 04PK.. - заголовок локального файла
PDF25 50 44 46 2D%PDF- - версия дальше
GIF87a/GIF89a47 49 46 38 37|39 61ASCII "GIF87a" или "GIF89a"

Проверьте сами: возьмите любую картинку .png и сделайте hexdump первых 8 байт - сигнатура совпадёт. Это чистая проверка типа по байтам, без магии.

Теперь немного «ручной работы» с битами. Часто настройки и флаги - это набор битов в одном числе. Примеры: права доступа в Unix, сетевые флаги, код состояния.

  • Быстрые операции с флагами (на Python):

    # флаги: 1=READ, 2=WRITE, 4=EXEC
    READ, WRITE, EXEC = 1, 2, 4
    perm = READ | WRITE            # установить READ и WRITE
    
    has_exec = (perm & EXEC) != 0  # проверить EXEC
    perm |= EXEC                   # добавить EXEC
    perm &= ~WRITE                 # убрать WRITE
    perm ^= READ                   # переключить READ
  • Права 644 в Unix - это 110 100 100 по тройкам: владелец rw-, группа r--, остальные r--. Быстро декодировать удобно в двоичном виде.

Аккуратно изменим байт в копии файла и посмотрим эффект. Делайте только на копиях - вы всё ломаете осознанно.

  1. Сделайте копию PNG и испортите 1 байт сигнатуры (macOS/Linux):

    cp image.png broken.png
    # XOR первого байта с 1: 0x89 -> 0x88
    python3 - <<'PY'
    with open('broken.png','r+b') as f:
        b = bytearray(f.read(8))
        b[0] ^= 0x01
        f.seek(0)
        f.write(b)
    PY
    hexdump -C broken.png | head -n1

    Большинство просмотрщиков и библиотек откажутся открывать broken.png: сигнатура не проходит проверку.

  2. Тот же трюк в Windows PowerShell:

    Copy-Item image.png broken.png
    $fs = [IO.File]::Open("broken.png", 'Open', 'ReadWrite')
    $bytes = New-Object byte[] 8
    $fs.Read($bytes,0,8) | Out-Null
    $bytes[0] = $bytes[0] -bxor 0x01
    $fs.Seek(0, 'Begin') | Out-Null
    $fs.Write($bytes,0,8)
    $fs.Close()
    Format-Hex .\broken.png -Count 8

Ещё один жизненный приём: проверяйте «сколько байт съел» ваш текст ввода/вывода.

  • Символ «Я» в UTF-8 - 2 байта. Проверка:

    printf 'Я' | wc -c      # 2
    printf 'Я' | hexdump -C # D0 AF
  • Эмодзи «😀» - 4 байта (U+1F600):

    printf '\xF0\x9F\x98\x80' | wc -c   # 4
    hexdump -C <<< $'\xF0\x9F\x98\x80'

Когда это реально помогает? В траблшутинге. Выяснить, почему сервис «ломает» строки (два лишних байта CRLF), почему JSON «не парсится» (скрытый BOM: EF BB BF), почему приложение ругается на «формат файла». Открыли дамп - увидели байты - приняли решение.

И финальный мостик: всё, что мы видим в дампе, в итоге исполняется как машинный код или трактуется как данные. Понимание уровня байтов даёт контроль: вы перестаёте гадать и начинаете проверять.

Написать комментарий