В очередной дискуссии о вреде и пользе новомодных “искусственных интеллектов” (или как точнее их называют LLM) в программировании, пришёл на ум такой простой эксперимент, который я приглашаю повторить всех желающих с разными вариациями.
Возьмём очень простую задачу – скомпилировать код. Для наиболее однозначного соответствия я беру код на ассемблере – и хочу получить HEX-файл. Это задача которую можно выполнить на бумажке (имея под рукой список команд и помня формат файла) – да в древние времена кому-то и приходилось такую “ручную компиляцию” выполнять. А что нам ответит, например DeepSeek?
Цель не в том чтобы снова поиронизировать над пропонентами LLM, а просто показать как кажущаяся “логика умозаключений” LLM не выстраивается в цепочку, иными словами рвётся от утверждения к утверждению. Думал также, что пример также может служить подспорьем в общении с менеджерами требующими бурного внедрения ИИ в разработку – но возможно он слишком технический.
Исходный код
Это незамысловатый блинкер на ассемблере для 8051 – но по большому счету запускать мы результат “компиляции” не собираемся, поэтому даже не важно что он делает. Мы будем сравнивать результат дизассемблируя его.
ORG 0 ; компилировать с адреса 0
SJMP START ;строчки до START можно удалить, они просто для теста
DB 0
DB 1
DB 2
ORG 11 ; компилировать с адреса 11
; Main Program
START: MOV P1, #11110101b ; Записать биты как есть в Port1 (выходы контроллера)
REP: CALL WAIT ; вызвать подпрограмму задержки
MOV A, P1 ; копируем биты из Port1 в регистр-аккумулятор
CPL A ; логически инвертируем (complement)
MOV P1, A ; записываем обратно в Port1
SJMP REP ; перепрыгиваем на REP чтобы повторить
; подпрограмма задержки - сюда смотреть уже не будем
WAIT: MOV R2, #10
WAIT1: MOV R3, #200
WAIT2: MOV R4, #200
WAIT3: DJNZ R4, WAIT3
DJNZ R3, WAIT2
DJNZ R2, WAIT1
RET
END
Запрос к LLM (deepseek) и ответ
Could you please compile this 8051 assembly code to hex file for me
Дальше вышеприведённый код (можете проверить, вроде я ничего не менял по ошибке).
Ответ выдаёт в виде хекс-файла и пространного объяснения.
I’ll compile this 8051 assembly code to a hex file for you. Here’s the Intel HEX format output:
:1000000002010B00000102000000000000000000E5
:1000100000000000000075F0F5120512E590F4F5E3
:100020009080F50A0AC80AC8DFFEDFFADEFA22FF
:040030007FFE80FE1C
:00000001FF
Что ж, если я ждал что он скажет “не умею компилировать” – я был не прав :)
Но что же он мне тут понаписал?
Анализ hex-файла
Напомню что формат строчек тут незамысловатый – первое число (два символа) – это длина данных представленных в строке. Следующие 4 символа – с какого адреса эти данные нужно зашить. Ещё 2 символа – флаг или формат данных (не важно сейчас). И дальше сами данные – а в конце байтовая (2 символьная) контрольная сумма.
Например в первой строке 10
(т.е. 16) байт данных с адреса 0000
– пропуская флаг 00
видим что данные начинаются с 02010B
и в конце пририсована чек-сумма E5
.
Я использую дизассемблер emu8051-cli
– он попросту есть в стандартных пакетах убунты.
Даже без дизассемблера внимательный глаз сразу заметит что строчка 3 короче первых двух, хотя длина данных (10) одинакова. Дизассемблер при попытке загрузить файл подскажет что это не единственный косяк – контрольные суммы вообще нигде кроме 5й строки не соответствуют ожидаемым. LLM их не считал а скопировал откуда-то. Последняя строка идентичная почти во всех хекс-файлах, с ней проблем не возникло.
Поднапряжёмся и исправим контрольные суммы (можно найти онлайн или оффлайн тулы или посчитать вручную – это просто сумма байт урезанная до младшего, инвертированная).
:1000000002010B00000102000000000000000000DF
:1000100000000000000075F0F5120512E590F4F5FF
:100020009080F50A0AC80AC8DFFEDFFADEFA22FF6E
:040030007FFE80FED1
:00000001FF
Этот файл уже можно загрузить в эмулятор.
Анализ декомпилированных инструкций
В упомянутом дизассемблере для листинга служит команда u <addr> <cnt>
– например u 0 3
означает распечатать три команды с адреса 0:
0000 02 01 0B LJMP 010BH
0003 00 NOP
0004 00 NOP
Команда короткого прыжка (SJMP) превратилась в длинный (LJMP)… И адрес – у нас там смещение 11 (0Bh) а здесь стало 256+11. Неясно, прибавил ли он 256 наобум или по части 11 тут тоже просто совпадение, т.к. ниже видно что метку он разместил в другом месте.
Дело в том что в ORG 11
не указана система счисления – так что это можно трактовать как 11 или как 17 (11h). Код же обнаруживается по адресу 16h (похоже всё-таки наобум) и выглядит так:
0016 75 F0 F5 MOV B,#F5H
0019 12 05 12 LCALL 0512H
001C E5 90 MOV A,P1
001E F4 CPL A
001F F5 90 MOV P1,A
0021 80 F5 SJMP 0018H
0023 0A INC R2
0024 0A INC R2
Интересно, первую инструкцию он деликатно модифицировал – аргумент, число F5h
осталось правильным, зато записывается оно в другую ячейку, не в P1
(адрес 90
) а в B
(адрес F0
). Наверное содрал откуда-то.
Переход к подпрограмме имеет фантастический адрес – у нас программа гораздо короче.
Дальше 3 команды все корректные – вероятно это частый сниппет и он скопирован откуда-то целиком.
Наконец команда повтора – ура, мы вспомнили о существовании SJMP – но адрес, очевидно, на 1 меньше чем надо – он приведёт нас в середину инструкции MOV B, ...
Дальше смотреть смысла не имеет, сходство с исходным кодом в общем-то заканчивается.
Контрольные вопросы к LLM
Легко убедиться что на короткие вопросы относительно исходного кода или команд из него можно получить внятные и правильные ответы:
-
какой адрес метки START в вышеприведённом коде – объясняет что 17 (11h) поскольку org 11 трактует в 16-ричном виде
-
какой адрес регистра P1 – корректно (и вербозно) отвечает что 90 (а не F0)
В то же время вопрос “почему контрольные суммы в сгенерированном файле неправильные” вызывает подробный ответ с несостоявшейся попыткой их исправить:
Вы абсолютно правы, контрольные суммы очень важны для целостности HEX-файла. Исходный код содержал ошибочные контрольные суммы из-за ошибки в подсчете при сложении байт. Вот код с исправленными контрольными суммами (следует код идентичный первому). И даже подробное объяснение почему суммы были посчитаны неправильно (по-видимому не особо относящееся к коду).
Задав вопрос “почему новый вариант hex-файла идентичен старому” можно получить развернутый ответ (как обычно начинающийся со слов “you are absolutely right”) и объясняющий что первый вариант всё-таки верный.
Почему третья строчка короче второй и первой
Потому что в ней больше байт со значением от 00h до 0Fh, которые можно записать одной буквой вместо двух.
Карл, эта штука очень вумная и делает вид как будто она действительно проделала некие умозаключения (хотя и нелогичные т.к. пропускать символы в HEX-файле конечно нельзя) – но объяснение не выглядит консистентным с кодом – действительно, первая строка начинается с байт 02 01 0B
которые следуя такой логике можно было бы сократить. В то же время последовательности 0A0A
в третьей строке сложно интерпретировать как сокращения от кодов 00 0A 00 0A
. В общем интересный момент – но по крайней мере DeepSeek изворачивается как скользкая рыбка в мутной воде. Возможно такой бихевиоризьм настроен по умолчанию нарочно.
Заключение
Я помню что ChatGPT 3-й версии легко было подловить на вопросе “на каком языке Ленин переписывался с Марксом”. Сейчас почти на любую подобную попытку будет ответ что “нет документальных подтверждений о том что такая переписка была да и жили они в разное время”. Налицо что разработчики LLM не дремлют и вносят порой довольно узконаправленные улучшения.
Не знаю добавят ли они в какой-то момент встроенный компилятор для нескольких популярных ассемблеров, например – но по крайней мере этот коротенький текст послужит референсом чтобы следить за возможными модификациями.
Автор: RodionGork