В очередной дискуссии о вреде и пользе новомодных «искусственных интеллектов» (или как точнее их называют 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 не дремлют и вносят порой довольно узконаправленные улучшения.
Не знаю добавят ли они в какой-то момент встроенный компилятор для нескольких популярных ассемблеров, например — но по крайней мере этот коротенький текст послужит референсом чтобы следить за возможными модификациями.
ссылка на оригинал статьи https://habr.com/ru/articles/899046/
Добавить комментарий