Упражнение на ассемблер 8051 для LLM — или «игра в испорченный компилятор». 8051.. 8051. Assembler.. 8051. Assembler. искусственный интеллект.. 8051. Assembler. искусственный интеллект. Программирование микроконтроллеров.

В очередной дискуссии о вреде и пользе новомодных “искусственных интеллектов” (или как точнее их называют 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

Источник

Рейтинг@Mail.ru
Rambler's Top100