Учебные материалы: механизм вместо тезисов, блоки «Факты для карточек», Ловушки (Claude Code) + diag/cards_src.tsv

This commit is contained in:
Kodlo-chan
2026-09-24 13:38:58 +07:00
parent e0ad8e0fee
commit 4259fbce75
17 changed files with 1688 additions and 115 deletions
+116
View File
@@ -32,5 +32,121 @@ int prefix_for_hosts(int hosts); // самая узкая п
Проверка: `python3 grade.py 13`. Критерий: все `ok`, сборка без предупреждений.
**Факты для карточек**
- base | Сколько узлов даёт /24? — 254
- base | Сколько узлов даёт /26? — 62
- base | Сколько узлов даёт /30? — 2
- base | Сеть, broadcast и диапазон узлов для `10.0.1.130/26`? — сеть `10.0.1.128`, broadcast `10.0.1.191`, узлы `129..190`
Почему дальше: чтобы получить эти числа программно, а не подбором, нужен механизм вычисления
маски, границ сети и обратной задачи — подбора префикса по числу узлов.
## Механизм: маска, границы сети, обратный подбор префикса
**Маска по префиксу.** Маска — это `prefix` единиц в старших битах и `32-prefix` нулей в
младших: `mask = 0xFFFFFFFFu << (32 - prefix)` для `prefix > 0`. Для `prefix == 0` формула
не годится напрямую — сдвиг на 32 бита для 32-битного типа в C/C++ является неопределённым
поведением (UB), поэтому этот случай обрабатывается отдельно: `mask = 0`.
**Границы сети.** `network = ip & mask` — обнуляет все биты хостовой части (сохраняет
только биты сети). `broadcast = network | ~mask` — выставляет все биты хостовой части в
единицу (`~mask` — это как раз маска хостовой части, инвертированная сетевая). Число бит,
отданных под узлы, — `32 - prefix`; всего адресов в блоке — `2^(32-prefix)`.
**Разбор эталонного примера `10.0.1.130/26`.** `/26` оставляет `32-26=6` бит под узлы,
блок из `2^6 = 64` адресов. Адрес `.130` попадает в блок `128..191` (границы блоков кратны
64): `network = 10.0.1.128`, `broadcast = 10.0.1.191`. Обычные узлы — `first_host =
network+1 = .129`, `last_host = broadcast-1 = .190`. Из 64 адресов блока минус 2 служебных
(сеть и broadcast) — 62 узла, что совпадает с `/26 → 62 узла`.
**Почему `/26` даёт именно 62 узла.** `host_count = 2^(32-26) - 2 = 2^6 - 2 = 64 - 2 = 62`.
Аналогично `/24`: `2^8 - 2 = 254`; `/30`: `2^2 - 2 = 2` — ровно минимум для соединения
точка-точка между двумя маршрутизаторами.
**`/31` — исключение (RFC 3021).** По общей формуле `/31` дал бы `2^1 - 2 = 0` узлов —
бессмысленный результат для блока из 2 адресов. RFC 3021 разрешает для каналов
точка-точка (ровно 2 узла на линке) отдавать под хосты оба адреса блока: широковещательный
адрес такому каналу физически не нужен (получателей всего два, и оба известны заранее), а
резервировать половину 2-адресного блока под network/broadcast — чистая потеря адресного
пространства. Поэтому `host_count = 2`, `first_host = network`, `last_host = broadcast`.
**`/32` — единичный адрес.** Блок из одного адреса: `network = broadcast = first_host =
last_host = ip`, `host_count = 1`. Используется для маршрутов на конкретный узел
(host route) или loopback-адресов маршрутизатора.
**Обратная задача — `prefix_for_hosts(h)`.** Нужен наибольший `prefix` (самая узкая
подсеть), при котором `2^(32-prefix) - 2 >= h`, то есть `2^(32-prefix) >= h+2`, то есть
`32-prefix >= log2(h+2)`. Так как число хостовых бит — целое, наименьшее подходящее
значение — `32-prefix = ceil(log2(h+2))`, откуда:
```
prefix_for_hosts(h) = 32 - ceil(log2(h+2))
```
Проверка на эталонных примерах: `h=62` → `h+2=64`, `log2=6`, `prefix=26` ✓. `h=63` →
`h+2=65`, `log2(65)≈6.02`, `ceil=7`, `prefix=25` ✓ (62 узла `/26` уже не хватает на 63-й
хост, нужен на бит шире — `/25` с 126 узлами). `h=254` → `h+2=256`, `log2=8`, `prefix=24` ✓.
`h=1` → `h+2=3`, `log2(3)≈1.58`, `ceil=2`, `prefix=30` ✓.
**Факты для карточек**
- base | Формула маски для префикса p (0<p≤32)? — `0xFFFFFFFF << (32-p)`; для p=0 маска = 0 отдельным случаем (сдвиг на 32 — UB)
- base | Как получить network и broadcast из ip и mask? — `network = ip & mask`, `broadcast = network | ~mask`
- core | Формула host_count для prefix ≤ 30? — `2^(32-prefix) - 2`
- core | Почему /31 — исключение и даёт 2 узла вместо 0 по общей формуле? — RFC 3021: у канала точка-точка ровно 2 узла, broadcast не нужен, поэтому оба адреса 2-адресного блока отдаются под хосты вместо потери половины блока
- core | Что возвращает subnet_of для /32? — network=broadcast=first_host=last_host=ip, host_count=1 (host route/loopback)
- core | Формула `prefix_for_hosts(h)`? — `32 - ceil(log2(h+2))`, из условия `2^(32-prefix) >= h+2`
- deep | Почему `prefix_for_hosts(63) = 25`, а не 26, хотя 62 меньше 63 всего на 1? — /26 даёт только 62 узла, этого не хватает даже на 1 хост меньше требуемых 63; нужно расширить хостовую часть на 1 бит — /25 с 126 узлами
## Ловушки
- Вычислить маску как `0xFFFFFFFF << (32 - prefix)` при `prefix == 0` без отдельной ветки
→ сдвиг на 32 бита для 32-битного типа — неопределённое поведение в C/C++ (компилятор
может дать любое значение, не обязательно 0) → UBSan ловит это как сдвиг за пределы
разрядности типа.
- Применить общую формулу `host_count = 2^(32-p) - 2` к `/31` без проверки исключения →
получится 0 узлов вместо 2 → тест на `/31` (RFC 3021) падает.
- Использовать `int` вместо `long long` для `host_count` → для `/0` значение `2^32 - 2` не
влезает в `int` (переполнение со знаком — UB) → в условии структуры явно указан `long long`
именно из-за этого случая.
- Перепутать host byte order с network byte order при сравнении с реальным трафиком или
утилитами вроде `tcpdump` → `10.0.0.1` в host order этой задачи — это `0x0A000001`, но в
сетевом порядке байты переставлены (`0x0100000A` при little-endian хосте) — конвертация
через `htonl`/`ntohl`, а не прямое сравнение чисел.
## Проверь себя
<details>
<summary>1. Почему /31 не подчиняется общей формуле «минус 2 служебных адреса»?</summary>
Потому что у 2-адресного блока резервирование network и broadcast по общей формуле оставило
бы 0 узлов — бессмысленно для канала точка-точка, где всего два участника и оба заранее
известны, широковещание не нужно. RFC 3021 явно отдаёт оба адреса блока под хосты.
</details>
<details>
<summary>2. Дано `ip=10.0.1.130`, `prefix=26`. Посчитать network, broadcast, first_host,
last_host по шагам.</summary>
Хостовых бит: `32-26=6`, размер блока `2^6=64`. Адрес `.130` попадает в блок, начинающийся
на границе, кратной 64: `128 <= 130 < 192`, значит `network=10.0.1.128`,
`broadcast=10.0.1.128+63=10.0.1.191`, `first_host=network+1=10.0.1.129`,
`last_host=broadcast-1=10.0.1.190`.
</details>
<details>
<summary>3. Почему `prefix_for_hosts(63) = 25`, а не 26, хотя 62 меньше 63 только на
единицу?</summary>
`/26` физически даёт ровно 62 адреса под хосты — этого не хватает даже на один хост меньше
требуемых 63. Следующий шаг «расширения» подсети — не +1 узел, а удвоение блока: снятие
одного бита из префикса (`/25`) сразу даёт 126 узлов. Промежуточных вариантов между /26 и
/25 не существует, поэтому единственный подходящий ответ — /25.
</details>
<details>
<summary>4. Почему для `prefix=0` нельзя вычислять маску как `0xFFFFFFFF << (32-0)`?</summary>
Это сдвиг на 32 бита для 32-битного беззнакового типа — стандарт C/C++ определяет сдвиг
только для величины меньше ширины типа в битах, сдвиг на саму ширину или больше — UB
(на практике часто даёт исходное значение без сдвига вместо ожидаемого 0). Поэтому
`prefix == 0` обрабатывается отдельной веткой: `mask = 0` напрямую.
</details>
Разбор после сдачи: как считать префикс по числу узлов за O(1) (`32 - ceil(log2(h+2))`),
почему `/31` — исключение, что такое маска в бинарном виде.