Почему одни и те же байты дают четыре разных результата CRC-16
CRC-16 — это название семейства, а не алгоритма, и члены этого семейства друг с другом не согласны. Передайте одни и те же байты в MODBUS, в CCITT-FALSE и в XMODEM — и получите три 16-битных числа, у которых нет ничего общего.
Какой именно член семейства работает, решают пять констант: poly, init, refin, refout и xorout. Достаточно поменять одну — и результат меняется целиком, без частичного сходства, которое могло бы подсказать, где ошибка. Большинство вопросов вида «почему мой CRC не сходится с тем, что выдает устройство» заканчивается именно здесь: две стороны считают разные варианты, и никто не записал, какие.
Каждое значение в этом руководстве получено на параметризованной модели CRC, запущенной на Python 3.14.7 под macOS. Перепроверка шла по
zlib.crc32иbinascii.crc_hqxиз стандартной библиотеки, по опубликованным контрольным значениям каталога RevEng CRC и по документации производителей для каждого набора параметров.
1. Один и тот же кадр — четыре результата CRC-16
Вот реальный запрос Modbus RTU. Адрес устройства 01, код функции 03 (чтение регистров хранения), начальный адрес 0x0000, количество 0x000A:
01 03 00 00 00 0A
Те же шесть байтов через четыре варианта:
| Вариант | Результат | В линии (младший байт первым) |
|---|---|---|
| CRC-16/MODBUS | 0xCDC5 | C5 CD |
| CRC-16/IBM-3740 (CCITT-FALSE) | 0x0428 | 28 04 |
| CRC-16/XMODEM | 0x0A38 | 38 0A |
| CRC-16/ARC | 0xD6C5 | C5 D6 |
Эти четыре значения ничем не связаны. Общего рисунка полубайтов у них нет, разница между ними не сводится к константе. Никакая перестановка тоже не превращает одно в другое. Два из них начинаются в линии с одного и того же байта — совпадение, и только.
Так что строка «CRC-16» в даташите не несет почти никакой информации. Она сообщает, что результат имеет ширину 16 бит, и на этом всё. Если при сверке с устройством, которое печатает двоичный вид, результат нужно посмотреть в другой системе счисления, конвертер систем счисления переводит 16-битное значение между шестнадцатеричным и двоичным представлением, и с ведущими нулями возиться не придется.
2. Что меняет каждый из пяти параметров
Под каждым вариантом лежит одна и та же машина: сдвиговый регистр, который проглатывает по одному биту за раз и подмешивает по XOR константу всякий раз, когда сверху выпадает единица. Параметры решают, что попадает в регистр и в какую сторону движутся биты, плюс один последний XOR на выходе.
Вот весь движок в четырнадцати строках на Python. Это побитовая реализация — медленная, зато читаемая, и она воспроизводит каждое значение в этой статье:
def crc(data, width, poly, init, refin, refout, xorout):
top = 1 << (width - 1)
mask = (1 << width) - 1
reg = init
for byte in data:
if refin:
byte = int(f"{byte:08b}"[::-1], 2)
reg ^= byte << (width - 8)
for _ in range(8):
reg = ((reg << 1) ^ poly) if reg & top else (reg << 1)
reg &= mask
if refout:
reg = int(f"{reg:0{width}b}"[::-1], 2)
return reg ^ xorout
Вывод движка можно сверить со стандартной библиотекой Python. zlib.crc32 и binascii.crc_hqx дают по независимому ответу, и все три совпадают:
zlib.crc32(b"123456789") = 0xCBF43926 engine above = 0xCBF43926 MATCH
binascii.crc_hqx(b"123456789", 0x0000) = 0x31C3 CRC-16/XMODEM = 0x31C3 MATCH
binascii.crc_hqx(b"123456789", 0xFFFF) = 0x29B1 CCITT-FALSE = 0x29B1 MATCH
poly: два лагеря, 0x8005 и 0x1021
Полином — это константа, которая по XOR возвращается в регистр. Записывают его в «нормальной» форме, со старшим битом по умолчанию, поэтому 0x8005 означает x^16 + x^15 + x^2 + 1, а 0x1021 — x^16 + x^12 + x^5 + 1. Цикл из сдвигов и XOR — это деление многочленов столбиком над GF(2), где полином и есть делитель. В регистре лежит текущий остаток, и каждый бит сообщения продвигает деление на один шаг.
Почти любой CRC-16, который встретится на практике, использует один из этих двух. 0x8005 покрывает ARC, MODBUS и USB. 0x1021 покрывает весь клубок CCITT плюс XMODEM, KERMIT и варианты для RFID. Но одного полинома мало, чтобы опознать вариант.
init: стартовое значение регистра
Господствуют два значения — 0x0000 и 0xFFFF, и разница между ними не косметическая. Если стартовать с нуля, нулевой байт оставляет регистр нулевым, поэтому 00 00 01 и 01 дают одинаковый CRC. Сообщение, которое по дороге приобрело или потеряло ведущие нули, проверку пройдет. Старт с 0xFFFF убирает эту слепую зону — потому его и выбрали Modbus, CCITT-FALSE и USB.
refin и refout: порядок битов, а не байтов
refin разворачивает восемь битов каждого входного байта перед тем, как тот попадет в регистр. refout разворачивает итоговое содержимое регистра перед последним XOR. Железу, которое выдвигает данные младшим битом вперед, эти отражения достаются бесплатно — поэтому отраженные варианты, как правило, происходят из последовательных протоколов.
Именно эту пару чаще всего путают с порядком байтов, хотя речь идет о разных уровнях. Раздел 6 разбирает эту путаницу отдельно.
xorout: финальный XOR
Это последний шаг, он применяется к регистру после refout. Для CRC-16 это обычно 0x0000 или 0xFFFF; в семействе CRC-32 используется 0xFFFFFFFF. Ошибиться в нем легче всего: расхождение здесь выглядит ровно так же, как расхождение в любом другом месте.
width: 8, 16 или 32 бита
Ширина задает потолок обнаружения. CRC шириной n ловит любую пачку ошибок длиной до n бит и пропускает случайное искажение с вероятностью примерно 2^-n. Это один случай из 65 536 для CRC-16 и один из 4,3 миллиарда для CRC-32.
Возьмем за отправную точку CRC-16/XMODEM и будем менять ровно по одному параметру на стандартном пробном входе 123456789:
baseline CRC-16/XMODEM = 0x31C3
init 0x0000 -> 0xFFFF (becomes CCITT-FALSE) = 0x29B1
refin/refout -> true (becomes KERMIT) = 0x2189
xorout 0x0000 -> 0xFFFF = 0xCE3C
poly 0x1021 -> 0x8005 = 0xFEE8
Один перевернутый флаг — и результат совершенно другой. У CRC нет понятия «близко». Два результата либо совпадают, либо расходятся, и величина расхождения ничего не говорит о том, насколько далеко разошлись входные данные. Разглядывать разницу в поисках причины бесполезно. Внутренний цикл не содержит ничего, кроме XOR и сдвигов; сами эти операторы разбирает полное руководство по побитовым операциям, так что повторять их здесь незачем.
3. «CCITT» — неудачное название
Каталог RevEng CRC на сентябрь 2026 года перечисляет 31 определение 16-битного CRC, и метку CCITT носят три из них. Ни одно из этих трех не совпадает с двумя другими. Четыре строки ниже собраны на общем полиноме и общем входе, а дают четыре не связанных между собой результата:
| Обиходное название | Название в каталоге | check | init | refin/refout | xorout |
|---|---|---|---|---|---|
| CCITT-FALSE | CRC-16/IBM-3740 | 0x29B1 | 0xFFFF | false | 0x0000 |
| XMODEM | CRC-16/XMODEM | 0x31C3 | 0x0000 | false | 0x0000 |
| «настоящий» CCITT | CRC-16/KERMIT | 0x2189 | 0x0000 | true | 0x0000 |
| — | CRC-16/MCRF4XX | 0x6F91 | 0xFFFF | true | 0x0000 |
История короткая и мало чем помогает. В каталоге CRC-16/KERMIT указан с псевдонимами CRC-CCITT и CRC-16/CCITT-TRUE, и этот вариант отраженный. Неотраженная реализация с init 0xFFFF тоже широко ходила под именем CCITT — поэтому каталог фиксирует «CCITT-FALSE» как псевдоним CRC-16/IBM-3740. XMODEM стоит между ними: тот же полином, отражения нет, init нулевой.
Поэтому, когда даташит говорит «CCITT», он сообщает только одно: полином равен 0x1021. Кандидатов по-прежнему четыре, а неверный выбор дает значение, которое выглядит ровно так же правдоподобно, как правильное.
Указывайте параметры, а не имена. Набор poly=0x1021, init=0xFFFF, refin=false, refout=false, xorout=0x0000 занимает в описании протокола одну строку и снимает вопрос. От записи «CRC-16/CCITT» толку нет.
4. Как определить, какой у вас вариант CRC-16
Каталог опознает варианты по отпечатку: у каждой записи опубликовано контрольное значение (check) — CRC от девяти ASCII-байтов 123456789. Прогоните эту строку через реализацию, которая перед вами, и найдите результат в таблице.
| Вариант | check | poly | init | refin | refout | xorout |
|---|---|---|---|---|---|---|
| CRC-16/ARC (IBM/LHA) | 0xBB3D | 0x8005 | 0x0000 | true | true | 0x0000 |
| CRC-16/MODBUS | 0x4B37 | 0x8005 | 0xFFFF | true | true | 0x0000 |
| CRC-16/USB | 0xB4C8 | 0x8005 | 0xFFFF | true | true | 0xFFFF |
| CRC-16/IBM-3740 (CCITT-FALSE) | 0x29B1 | 0x1021 | 0xFFFF | false | false | 0x0000 |
| CRC-16/XMODEM | 0x31C3 | 0x1021 | 0x0000 | false | false | 0x0000 |
| CRC-16/KERMIT (настоящий CCITT) | 0x2189 | 0x1021 | 0x0000 | true | true | 0x0000 |
| CRC-16/GENIBUS | 0xD64E | 0x1021 | 0xFFFF | false | false | 0xFFFF |
| CRC-16/MCRF4XX | 0x6F91 | 0x1021 | 0xFFFF | true | true | 0x0000 |
| CRC-32/ISO-HDLC (zip, PNG, zlib) | 0xCBF43926 | 0x04C11DB7 | 0xFFFFFFFF | true | true | 0xFFFFFFFF |
| CRC-32/BZIP2 | 0xFC891918 | 0x04C11DB7 | 0xFFFFFFFF | false | false | 0xFFFFFFFF |
| CRC-32C (Castagnoli, iSCSI) | 0xE3069283 | 0x1EDC6F41 | 0xFFFFFFFF | true | true | 0xFFFFFFFF |
| CRC-8/SMBUS | 0xF4 | 0x07 | 0x00 | false | false | 0x00 |
| CRC-8/MAXIM-DOW (1-Wire) | 0xA1 | 0x31 | 0x00 | true | true | 0x00 |
Больше всего сравнений ломается на одной детали: 123456789 означает девять байтов 31 32 33 34 35 36 37 38 39, а не число 123456789 и не строку с завершающим нулем. Если язык передает в функцию широкую строку или дописывает терминатор, на вход идут другие байты и мимо пройдет каждая строка таблицы. Данные не в ASCII добавляют вторую ловушку: один и тот же текст в UTF-8 и в Windows-1252 — это две разные последовательности байтов, а значит, и два разных CRC. Таблица ASCII и конвертер показывает байт за каждым символом, так что на проверку уходят секунды.
Если контрольное значение не совпадает ни с одной строкой, сначала исключите скучные причины, а уже потом подозревайте самодельный полином:
- Результат читается из линии в обратном порядке — тогда дело в порядке байтов, а не в параметрах.
- Отправитель включает в расчет байт адреса, байт длины или само поле CRC. Или, наоборот, исключает.
- Фирменный
init, не равный ни0x0000, ни0xFFFF. Такое встречается в проприетарных протоколах счетчиков и обычно закопано в сноску.
Перебор параметров
Когда производитель не отвечает, а прошивку не прочитать, попросите одно: перехваченный кадр вместе с CRC, который выдало для него устройство. Дальше — перебор. Два полинома, два значения init, независимые refin и refout и два значения xorout дают 32 комбинации, то есть практически ничего:
target = 0x4B37 # значение, которое вернула их реализация
for poly in (0x8005, 0x1021):
for init in (0x0000, 0xFFFF):
for refin in (False, True):
for refout in (False, True):
for xorout in (0x0000, 0xFFFF):
if crc(b"123456789", 16, poly, init, refin, refout, xorout) == target:
print(f"poly=0x{poly:04X} init=0x{init:04X} "
f"refin={refin} refout={refout} xorout=0x{xorout:04X}")
poly=0x8005 init=0xFFFF refin=True refout=True xorout=0x0000
Одно попадание, и это CRC-16/MODBUS. Подставьте вместо 123456789 реальный кадр — и тот же перебор из 32 вариантов сработает на любом сообщении, для которого есть заведомо верный CRC. Если после одного образца выживает несколько комбинаций, возьмите второй кадр и пересеките результаты.
5. Modbus RTU на практике
Modbus RTU использует CRC-16/MODBUS: полином 0x8005, init 0xFFFF, отражение на входе и на выходе, финального XOR нет. Для нашего шестибайтового запроса CRC равен 0xCDC5, а готовый кадр выглядит так:
01 03 00 00 00 0A C5 CD
В линии байты идут в обратном порядке относительно записи значения
Значение — 0xCDC5. К кадру дописываются байты C5 CD. Modbus предписывает передавать младший байт CRC первым, то есть в порядке, обратном тому, в каком читается шестнадцатеричное число, и на этом спотыкаются постоянно. Все остальные многобайтовые поля того же кадра, включая счетчик регистров 00 0A, идут в big-endian. CRC — исключение.
Побочный эффект у такого порядка полезный. Посчитайте CRC-16/MODBUS по всему кадру, вместе с байтами CRC, и результат будет такой:
0x0000
Это и есть вся процедура проверки на стороне приемника. Не нужно отрезать два хвостовых байта, менять их местами и сравнивать с локально посчитанным значением. Достаточно прогнать CRC по всему, что пришло, и проверить на ноль. Меньше шагов — меньше мест, где можно перевернуть байты не в ту сторону.
Почему в документации по Modbus стоит 0xA001
Откройте почти любую реализацию Modbus — и в исходниках будет константа 0xA001, а не 0x8005. Верны обе. 0xA001 — это 0x8005 с развернутыми 16 битами. Она принадлежит отраженной форме алгоритма: регистр сдвигается вправо, а не влево, и разворачивать входные байты по отдельности уже не нужно. Результат у обеих реализаций одинаковый, различаются только внутренности. 0xA001 в исходниках — надежный признак отраженной реализации CRC-16.
Modbus ASCII — отдельный формат кадра, и в дампе он виден сразу: каждый байт передается двумя шестнадцатеричными символами между ведущим 3A (:) и завершающими 0D 0A, а вместо CRC там используется LRC. Если в трассировке видно 3A 30 31 30 33 там, где ожидались 01 03, значит, включен режим ASCII, и никакой вариант CRC никогда не сойдется. Таблица ASCII переводит эти байты обратно в символы.
Тот же алгоритм на C
Прошивки Modbus почти никогда не используют показанную выше модель на Python. Они работают прямо с отраженной формой: сдвиг вправо и XOR с 0xA001:
uint16_t crc16_modbus(const uint8_t *buf, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= buf[i];
for (int b = 0; b < 8; b++)
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
return crc;
}
На кадре 01 03 00 00 00 0A функция возвращает 0xCDC5 — то же значение, что дал движок на Python. Контрольная строка 123456789 дает 0x4B37. Оба результата получены под Apple clang 21 и совпадают с таблицей из раздела 4 бит в бит.
6. refin/refout — это не порядок байтов
Эти две вещи живут на разных уровнях, и путаница обходится дорого: в Modbus присутствуют обе сразу.
Порядок байтов — это про байты внутри многобайтового значения: хранится и передается ли 0xCDC5 как CD C5 или как C5 CD. Внутри байта ничего не двигается. Этот уровень целиком разбирает big-endian против little-endian, и пересказывать его здесь незачем.
Отражение — это про биты внутри одного байта. refin разворачивает восемь битов каждого байта до того, как его увидит регистр: бит 0 становится битом 7. Свое место в сообщении байт сохраняет. Друг на друга эти две настройки не влияют.
В кадре Modbus происходит и то и другое, независимо друг от друга:
- Внутри алгоритма
refinиrefoutравны true, поэтому биты в каждом байте разворачиваются. - В линии готовый CRC дописывается младшим байтом вперед — это решение о порядке байтов, принятое протоколом.
Отключите отражение, чтобы «починить» порядок байтов, — и получите совершенно другой вариант. Переставите хвостовые байты кадра в компенсацию за несовпадение отражения — значение окажется неверным по-новому. Диагностируйте по одному: сначала добейтесь верного контрольного значения на голом алгоритме, потом займитесь раскладкой кадра.
7. CRC-32 против CRC-16: что выбрать?
У CRC-32 та же проблема, что и у CRC-16, просто менее заметная: один вариант доминирует настолько, что большинство разработчиков так и не узнает о существовании остальных.
| Вариант | check | poly | refin/refout |
|---|---|---|---|
| CRC-32/ISO-HDLC | 0xCBF43926 | 0x04C11DB7 | true |
| CRC-32/BZIP2 | 0xFC891918 | 0x04C11DB7 | false |
| CRC-32C (Castagnoli) | 0xE3069283 | 0x1EDC6F41 | true |
Именно ISO-HDLC считает zlib.crc32, и на веб-стороне стека он встречается повсюду. Zip хранит его для каждой записи в центральном каталоге, а каждый чанк PNG заканчивается таким CRC, покрывающим тип чанка и данные. Ethernet ставит контрольную последовательность кадра с тем же набором параметров в конец каждого кадра. BZIP2 — это тот же полином с выключенными отражениями, и он дает значение, не имеющее с ISO-HDLC ничего общего.
CRC-16 встречается по соседству тоже: Redis Cluster направляет ключ в один из своих 16 384 слотов по формуле CRC16(key) mod 16384, и поэтому ключи с одинаковым хеш-тегом попадают на один узел.
Почему CRC-32C выиграл аппаратную лотерею
CRC-32C использует другой полином, который лучше обнаруживает ошибки на коротких блоках — а именно такие отправляют протоколы хранения и сети. За это его взяли iSCSI, контрольные суммы метаданных ext4, SCTP и Btrfs. Потом Intel перенес его в кремний: инструкция crc32 из SSE4.2 считает CRC-32C напрямую, и на современных x86 проверка целостности обходится почти даром. Если для нового кода выбирать CRC сегодня и ограничений совместимости нет, брать стоит именно его.
Ни один из них не годится на роль хеша для проверки скачанного файла. Когда нужна контрольная сумма, подтверждающая, что файл дошел целым, привычный инструмент — генератор хешей MD5, а MD5 против SHA-256 разбирает, какому дайджесту и в каком случае доверять. CRC отвечает на вопрос «не испортилось ли это по дороге»; криптографический хеш — на вопрос «это ровно то содержимое, которого я жду».
8. CRC не защищает от подмены
Вот два сообщения JSON с противоположным смыслом и одинаковым CRC-32:
message A = b'{"to":"alice","amount":100} H\xf6\xc0E'
message B = b'{"to":"mallory","amount":999}\xb2\xb2\xc2\xc2'
CRC-32(A) = 0x12345678
CRC-32(B) = 0x12345678
SHA-256 от тех же двух:
A -> a83b90f5e3f21dd32039e0e693a8b15864eda83e258c115deeee50a60c09ae23
B -> f9319ee0507d719c7260535dff33df721ddd9c0e1857690f3d8e02f92601b742
Ничего здесь не подбиралось перебором. Хвостовые байты вычислены алгебраически. CRC — линейная функция, и дописывание четырех байтов к сообщению отображается на выход CRC-32 биекцией: для любого сообщения и любого целевого значения существует ровно один четырехбайтовый суффикс, который туда приводит, а его поиск — это арифметика.
Дописываем к hello world четыре вычисленных байта 46 C9 6E 0B:
CRC-32(b'hello world' + 46 C9 6E 0B) = 0xDEADBEEF target 0xDEADBEEF MATCH
Значит, атакующий, который может изменить payload, заодно поправит и CRC — за константное время, ничего не подбирая. Секрет в начале сообщения не спасает: при правке, сохраняющей длину, поправка к CRC зависит только от изменившихся битов, поэтому ее можно вычислить, так и не узнав секрет. Ровно так была устроена атака, сломавшая WEP.
CRC защищает от шума в канале — случайного и без цели. Противник действует не наугад и с конкретной целью, и против него CRC не дает ничего. Когда требуется подлинность, а не целостность, нужна конструкция с ключом: генератор HMAC считает его для любого payload и ключа, а почему не проходит проверка подписи webhook разбирает места, на которых спотыкаются при подключении к реальному endpoint.
9. Частые вопросы
Является ли CRC-16/CCITT тем же самым, что и CRC-16/CCITT-FALSE?
Нет, CRC-16/CCITT-FALSE и CRC-16/CCITT — разные варианты. CCITT-FALSE — это CRC-16/IBM-3740: init 0xFFFF, без отражения, контрольное значение 0x29B1. Вариант, который обычно имеют в виду под простым CCITT, — это CRC-16/KERMIT: init 0x0000, с отражением, check 0x2189. Общий у них только полином 0x1021, во всем остальном они расходятся.
Почему онлайн-калькулятор не сходится с моим устройством?
Почти всегда дело в разных параметрах. Инструмент по умолчанию считает один вариант, а устройство реализует другой. Прогоните ASCII-байты 123456789 через оба, сравните два контрольных значения с таблицей из раздела 4 — и разошедшийся параметр обычно находится меньше чем за минуту.
Почему Modbus передает младший байт CRC первым?
Потому что так написано в спецификации, и это единственное поле кадра, которое ведет себя таким образом. Адреса регистров и счетчики идут в big-endian, а хвостовой CRC — нет. Считайте CRC-16/MODBUS по всему кадру, включая эти два байта, и проверяйте на 0x0000, вместо того чтобы переставлять их вручную.
Что именно разворачивают refin и refout?
Биты внутри байта и никогда — байты внутри сообщения. refin разворачивает восемь битов каждого входного байта перед обработкой; refout разворачивает итоговый регистр перед последним XOR. Оба не зависят от порядка байтов, который определяет, как многобайтовое значение раскладывается в линии.
Что выбрать — CRC-16 или CRC-32?
CRC-16 пропускает случайное искажение примерно один раз из 65 536, CRC-32 — один раз из 4,3 миллиарда. Для коротких последовательных кадров в несколько десятков байтов CRC-16 достаточно, и обычно его в любом случае предписывает протокол. Для файлов, сетевых кадров и всего, что больше нескольких килобайт, берите CRC-32.
Можно ли использовать CRC как подпись для API?
Нет, CRC не может служить подписью для API. CRC линеен, поэтому любой, кто изменит payload, пересчитает подходящее значение, а дописывание четырех подобранных байтов приводит к любому заданному значению CRC-32. Подписи требуют секретного ключа и нелинейной конструкции. Используйте HMAC с SHA-256.
Можно ли восстановить исходные данные по значению CRC?
Нет, исходные данные по значению CRC не восстанавливаются. CRC-16 сжимает любой вход до 16 бит, поэтому каждому значению соответствует бесконечно много сообщений. Обратное направление тем не менее полезно атакующему: по заданному значению можно построить сообщение, которое его дает, — ровно поэтому CRC ничего не удостоверяет.
В документации производителя написано только «CRC-16». Как определить параметры?
Попросите один перехваченный кадр и CRC, который посчитало для него их устройство, а затем прогоните по этой паре перебор из 32 комбинаций, показанный в разделе 4. Если выживает больше одного набора параметров, повторите со вторым кадром и оставьте пересечение. Двух образцов почти всегда достаточно.