Основной задачей было уменьшение размера программы, т.к. использовали микроконтроллер с небольшим объемом памяти, а функциональность изделия должна быть большой. По этому появилась идея использовать анализатор кода, поиск в интернете ничего не дал, по этому пришлось делать самостоятельно.

Решил поделиться идеями, так как думаю что может кто-то напишет более приличную программу для анализа программы на ассемблере 8051.

В этой статье опишу основные этапы получившегося анализатора. Часть этих этапов можно использовать для анализа программ написанных на других языках.

Этап 1. Сначала необходимо преобразовать исходный текст программы к максимально простому виду. С кодом программы из которого удалили все лишнее удобнее работать.

Пример:

Function_ADD:

                mov       A,Peremenay1

                add         A,#Constanta1

                mov       Peremenay2,A

приводим к виду

code_0001: mov A,020h

                add         A,#030h

                mov       021h,A

Для этого решил сделать дизасемблирование из HEX-файла. Так это показалось гораздо удобнее, чем обрабатывать исходник, к тому же у скомпилированного файла уже код распределен по памяти, что пригодиться в дальнейшим.

Этап 2. Создал таблицу, в которую занес строки исходной программы,  тип команды, тип операндов, адрес в памяти и т.д.

Пример:

Номер строки|Адрес перехода(если есть) | Название операции |Операнд 1| Операнд 2| Адрес перехода| Тип операнда 1 | Тип операнда 2|

1|code_0001|mov|A|020h| |ACC|DIR|

2| |add|A|030h| |ACC|CONST|

3| |mov|021h|A| |DIR|ACC|

Чем больше заполненных столбцов с различными возможными вариантами, тем будет проще делать анализ.

Этап 3. Переходим непосредственно к анализу. Пока сделал анализ двух соседних команд, алгоритм такого анализа довольно простой, по определенным правилам проверяем команды.

Пример:

1. Замена команд вызова процедур

LCALL code_0001

RET

можно заменить командой

LJMP code_0001

это даст экономию 1-го байта.

2. Замена команд присвоения

MOV A,#CONSTANTA

MOV @R0,A

можно заменить командой

MOV @R0, #CONSTANTA

или

MOV A,@R0

MOV RAM,A

можно заменить командой

MOV RAM,@R0

3. Замена команд перемещения

JNZ label1

SJMP label2

label1:

.................

label2:

можно заменить командой

JZ label2

label1:

..........

label2:

И таких вариантов достаточно много, когда пишется программа она не всегда пишется с учетом экономии места, а так чтобы было в дальнейшем понятно.

Этап 4. Посчитать сколько обращений по каждому адресу перехода, задача найти единичные обращения. Также смотрим, какая команда стоит перед адресом перехода в программе. На основе этих данный добавляем в таблицу дополнительный данные касательно переходов.

Пример:

                MOV A,030h

                MOV 020h,A

                ret

code_0001: MOV A,020h

Значит возможно перенести часть программы начиная с адреса перехода "code_0001"  ближе к команде  обращения к этому адресу или вообще убрать команду перехода, а за место нее перенести часть программы.

Также есть нулевые обращения, это когда к определенному адресу можно попасть только через команду перехода, а ее нет в программе, например как в примере, перед командой с адресом "code_0001" стоит команда «RET».

Этап 5. Посчитать сколько раз используется каждая ячейка памяти данных (ОЗУ), и как используется (исходные данные для команды или изменение данных в ячейки).

Например может оказаться, что ячейку памяти только изменяют, при этом ее нигде не используют как исходные данные. Можно избавиться от команд которые изменяют такую ячейку.

Этап 6. Компиляция исходного кода. После компиляции возможно проверить количество байт между командой перехода и адресом перехода, например для того чтобы сместить часть кода поближе к команде перехода, заменив ее с LJMP на SJMP.

При использование такого анализа получил результат, что 8-ми килобайтную программу удалось уменьшить на 300 байт.

Дальнейшие планы, сделать анализ кода на основе его функционирования, т.е. что делает определенная часть программы и как ее можно заменить.

Комментарии (5)


  1. lorc
    02.12.2021 01:37
    +1

    Круто!

    Сначала хотел предложить сравнить это с оптимизиющим сишным компилятором, но потом почитал про 8051, узнал что ни gcc ни llvm эту архитектуру не поддерживают. Что впрочем и не удивительно: три адресных пространства с разной шириной адреса - это вам не это...


    1. VelocidadAbsurda
      02.12.2021 02:55
      +3

      Для 8051 есть прямо таки чемпионский в плане оптимизации компилятор от IAR, умеющий делать и то, что описал автор, и многое другое (находить одинаковые последовательности кода и выносить в функции, выделять одинаковые концовки функций, сжимать секцию данных, анализировать длины путей исполнения итд), вот с ним бы интересно сравнить.


  1. gleb_l
    02.12.2021 01:48
    +1

    Если дизассемблировать код после компилятора C, то да - можно выгадать немало адресного пространства CSEG - я делал себе среду дизассемблирования для 8051 наподобие IDA ещё в 90х годах - там при генерации ассемблерного листинга можно было сразу применять эти эвристики. Call/ret -> jmp, jmp/ret -> ret, был даже jmp/jmp/ret -> ret. Но у меня, как и в IDA, каждый байт сегмента имел метатег типа - который определялся автоматически для кода по дереву исполнения, и мог к тому же доопределяться или переопределяться вручную. Поэтому дизассемблер знал для всего размеченного сегмента, где код, а где, например, константные указатели на данные или подпрограммы - это делало оптимизацию (и следующее за ней перемещение) безопасными. Парсинг hex-файла же потенциально опасен - вы не сможете отличить код от похожих на него константных данных, и либо их “оптимизируете”, либо подвинете код без корректировки константных указателей - и результат окажется неработоспособен.


  1. sok
    02.12.2021 05:59
    +1

    У меня есть схожий проект по оптимизации некоторого ассемблерного кода. Использую метод перебора\ГА.

    1. Закладываем тестами весь требуемый функционал

    2. Пишем(или даже не пишем) код решающий данный функционал

    3. Запускаем ГА который наугад модифицирует код, переставляет команды и прочее

    После каждой модификации прогоняются тесты и если они проходят, а код "оптимальнее" - код лидера подменяется найденным решением.

    Работает медленно, приходится писать свой интерпретатор и эмулятор устройства, есть некоторые особенности по работе с адресами.

    Но хорошо параллелится, процесс поиска легко разносится на вычислительный кластер, а код получается действительно отменный.

    Делал как часть исследовательской работы для задач работы, но так и не применил, сменил работу, жду когда пригодится на новой.


  1. vkni
    02.12.2021 07:57

    Для этого решил сделать дизасемблирование из HEX-файла. Так это показалось гораздо удобнее, чем обрабатывать исходник

    Странно, парсер ассемблера, в общем, несложен.

    Проект напоминает самый самый конец backend'а компилятора, уже после того, как выбраны инструкции.