Files
eduplan-cpp-eltex/diag/tasks/01_bits/task.md
T

8.1 KiB
Raw Blame History

Задача 01 — битовые операции (разминка)

Первая задача дня: руки размялись, потом основная. Ориентир — 20–30 минут.

Глава I. Общая информация

  • Цель: уверенно работать с битами — это ежедневный инструмент в embedded, сетях и протоколах.
  • Почему это в Eltex: заголовки пакетов, маски, флаги, регистры — всё битовое. Вопросы вида «посчитай единичные биты» и «поменяй порядок байт» на собеседовании почти гарантированы. На практике bswap нужен ровно потому, что сеть передаёт многобайтовые поля в network byte order (big-endian), а x86 внутри — little-endian: без разворота байт число читается неверно.
  • Что сдаётся: solution.cpp, проходящий python3 grade.py 01.

Глава II. Что нужно знать до старта

Четыре инструмента, которых достаточно:

  1. x & (x - 1) — гасит самый младший единичный бит. Механизм: в дополнительном коде x - 1 переворачивает все нули справа от младшего единичного бита в единицы, а сам этот бит — в ноль, биты выше не трогает; операция & с исходным x поэтому обнуляет ровно один бит — младший единичный. Отсюда классика: число единиц можно считать циклом, пока x не станет нулём, и число итераций равно числу единичных бит, а не 32.
  2. x & 1 — младший бит; x >> 1 — сдвиг вправо.
  3. x & (1u << k) — проверка k-го бита.
  4. Маски: 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000 — это четыре байта 32-битного числа. Комбинация «сдвинул и сложил» даёт любой порядок байт.

Почему дальше: та же логика масок и сдвигов нужна в задаче 04 — разбор IPv4-заголовка byte-по-byte.

Глава III. Задание

Реализуй в solution.cpp четыре функции. Встроенные __builtin_popcount, std::popcount, std::byteswap использовать нельзя — нужна своя реализация (любая корректная).

int      popcount32(uint32_t x);        // число единичных бит
uint32_t reverse_bits32(uint32_t x);    // развернуть порядок всех 32 бит
bool     is_power_of_two(uint32_t x);   // x — степень двойки (0 не считается)
uint32_t bswap32(uint32_t x);           // поменять порядок байт местами

Полезно сразу проверять себя руками: bswap32(0x11223344) == 0x44332211, popcount32(0b1011) == 3, is_power_of_two(1) == true, is_power_of_two(0) == false, reverse_bits32(1) == 0x80000000.

Глава IV. Ступени

Ступень 1 (5 минут). is_power_of_two — самая простая. Подумай, чем степень двойки отличается от остальных чисел: у неё ровно один единичный бит. Значит подойдёт приём x & (x - 1). Отдельно обработай 0 (он не степень двойки) и проверь на 1 и на 0x80000000.

Ступень 2 (10 минут). popcount32 — цикл: пока x != 0, гаси младший бит и считай итерации. Альтернатива — 32 сдвига с проверкой младшего бита; оба варианта верные, первый в среднем быстрее.

Ступень 3 (10 минут). reverse_bits32 — цикл на 32 итерации: берём младший бит x, ставим его в результат на позицию 31 - i, сдвигаем x. Внимание на типы: сдвиг 1u << 31 для знакового int — UB, поэтому работай в uint32_t.

Ступень 4 (10 минут). bswap32 — четыре байта. Самый наглядный вариант: маски и сдвиги, ((x & 0x000000FFu) << 24) | ((x & 0x0000FF00u) << 8) | ((x & 0x00FF0000u) >> 8) | ((x & 0xFF000000u) >> 24).

Ступень 5. python3 grade.py 01 — пока не станет PASS.

Глава V. Критерии приёмки

PASS от grade.py 01: все проверки ok, сборка без предупреждений, ASAN/UBSAN чистые.

Глава VI. Подсказки (после первой попытки)

UBSAN жалуется на сдвиг

Сдвиг знакового числа в старший бит — неопределённое поведение. Все промежуточные значения держи в uint32_t и пиши константы с суффиксом u: 1u << 31.

is_power_of_two возвращает true на нуле

У нуля нет единичных битов, поэтому x & (x-1) даёт 0 — как и у степеней двойки. Добавь явную проверку x != 0.

reverse_bits32 работает «почти»

Проверь порядок операций в цикле: сначала ставим бит в результат, потом сдвигаем источник. Если перепутать — биты уедут на одну позицию.

Глава VII. Ловушки

  • Сдвиги в int вместо uint32_t → сдвиг 1 << 31 для знакового типа — UB → UBSAN падает на этой строке с диагностикой конкретного сдвига.
  • Забыт ноль в is_power_of_two → 0 & (0 - 1) == 0, функция возвращает true на нуле → тест на x == 0 красный.
  • В bswap32 перепутаны направления сдвигов (влево/вправо) → байты встают не на свои места → bswap32(0x11223344) != 0x44332211, видно прямым сравнением.
  • Использование __builtin_popcount — запрещено условием: смысл в том, чтобы написать самому; формально тест это не всегда ловит по выводу, но нарушает условие задачи.

Факты для карточек

  • base | Сколько байт меняет местами bswap32? — 4
  • core | Что делает x & (x - 1)? — гасит младший единичный бит x
  • core | Сколько итераций цикла popcount32 на x = 0xFFFFFFFF? — 32 (по числу единичных бит)
  • deep | Почему 1u << 31 пишут с суффиксом u, а не как int? — сдвиг знакового int в знаковый бит — UB, uint32_t определён стандартом для любых сдвигов в пределах разрядности
  • base | Какой порядок байт использует сеть для многобайтовых полей? — network byte order (big-endian)

После сдачи

Разбор: почему x & (x-1) гасит младший бит; как эти приёмы используются в реальных протоколах (маски подсетей, флаги TCP-заголовка, разбор полей IPv4).