Русский
Русский
English
Статистика
Реклама

Циклы

Дизассемблируем код на Си switch() case assembler disass()

19.11.2020 20:15:16 | Автор: admin

Доброго времени суток.

Сегодня мы будем смотреть дизассемблированный код инструкций if, for, while, swich, которые написаны на языке Си.

1605795529033.png1605795529033.png

Инcтрукция if

Данную инструкцию довольно просто отличить в дизассемблированном виде от других инструкций. Её отличительное свойство - одиночные инструкции условного перехода je, jne и другие команды jump.

1605790962062.png1605790962062.png1605790989407.png1605790989407.png

Напишем небольшую программу на языке Си и дизассемблируем её с помощью radare2. Разницы между IDA PRO и radare2 при дизассемблировании этих программ не было обнаружено, поэтому я воспользуюсь radare2. Вы можете использовать IDA PRO.

IDA PRO

2020-11-17_08-22.png2020-11-17_08-22.png2020-11-17_08-23.png2020-11-17_08-23.png

radare2

1605792900362.png1605792900362.png

Код на Си

#include <stdio.h>void main() {    int x = 1;    int y = 2;    if(x == y) {        printf("x = y\n");    }    else{        printf("x != y\n");    }}

Компилируем при помощи gcc. Команда gcc -m32 prog_if.c -o prog_if. -m32 озночает, что компилироваться код будет под архитектуру x86.

Чтобы посмотреть на код в radare2, напишем команду r2 prog_if. Далее прописываем aaa для анализа кода и переходим к функции main s main. Посмотрим на код с помощью команды pdf.

Дизассемблированный вариант в radare2

1605792900362.png1605792900362.png

Первым делом в программе происходит объявление переменных ( int x; int y ), а затем значение 1 перемещается в varch (это переменная x) и значение 2 в var10h (это переменная y). Далее идёт сравнение (cmp) 1 и 2 (cmp edx, dword [var_10h]). Эти значения не равны. Значит jne ( jump if noe equal) перейдёт по адресу 0x000011e1. Проще всего инструкцию if запомнить и опрелелить в режиме графов (команда VV для для radare2 или клавиша пробел для IDA).

Режим графов

1605792914861.png1605792914861.png

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

Код на Си

#include <stdio.h>void main() {    int x = 0;    int y = 1;    int z = 2;    if(x == y) {        if(z == 0) {        printf("z = 0; x = y\n");        }        else{            printf("z = 0; x != y\n");        }    }    else {        if(z == 0) {            printf("z = zero and x != y.\n");        } else {            printf("z non-zero and x != y.\n");        }    }}

Дизассемблированный вариант в radare2

1605792935104.png1605792935104.png

Режим графов

1605792950680.png1605792950680.png

В режиме графов это воспринимать намного проще.

Инструкция for

Циклы for всегда состоят из четырех этапов: инициализации, сравнения, выполнения инструкций и инкремента/декремента. По этим четырём этапом мы будем распозновать for в ассемблерном коде.

Код на Си

#include <stdio.h>void main() {    int x;    for(x = 0; x < 100; x++) {        printf("x = %d", x);    }}

Дизассемблированный вариант в radare2

1605792973718.png1605792973718.png

1 - инициализации переменной var_ch (x = 0)
2 - сравнение, а затем jle. ( пока x не будет меньше или равен 2, выполнять цикл.)
3 - выполнения инструкций (printf)
4 - инкрмент переменной var_
ch (++x)

Режим графов

1605792987678.png1605792987678.png

Инструкция while

Цикл while часто используется при ожидании, пока не будет выполнено какое-то условие, например получение команды или пакета. В ассемблере циклы while похожи на for, но их легче понять. В ассемблере это выражение похоже на цикл for, но инкремента может и не быть.

Код на Си

#include <stdio.h>int func_1(int x);int change_status();int main() {    int status = 0;    while(status == 0) {        printf("int e = %d", func_1(5) );        status = change_status();    }    return 0;}int change_status() {    return 1;}int func_1(int x) {    int c;    int e;    int l;    c = 1 + 2;    e = x / 5;    l = 4 - 2;    return e;}

Дизассемблированный вариант в radare2

1605793002542.png1605793002542.png

1 - инициализации переменной var_4h (status = 0)
2 - сравнение, а затем je. ( пока x равен 0, выполнять цикл.)
3 - выполнения инструкций (func
1, printf, change_status)

Режим графов

1605793019409.png1605793019409.png

Инструкция switch

Конструкция switch обычно компилируется двумя способами: по примеру условного выражения или как таблица переходов.

Компиляция по примеру условного выражения

Код на Си

#include <stdio.h>int main() {    int i = 3;    switch(i) {        case 1:            printf("CASE_1 i = %d", i+4);            break;        case 2:            printf("CASE_2 i = %d", i+9);            break;        case 3:            printf("CASE_3 i = %d", i+14);            break;    }    return 0;}

Дизассемблированный вариант в radare2

1605793045836.png1605793045836.png

1 - инициализации переменной var_4h (i = 3)
2 - выполнения инструкций (add, printf)

Чтобы понять какой "case" выбран, происходит сравниение (cmp, а затем je, jne) переменной i с значением case.

Режим графов

1605793347813.png1605793347813.pngscreen13.pngscreen13.pngscreen_13_2.pngscreen_13_2.png

Глядя на этот код, сложно (если вообще возможно) сказать, что представлял собой оригинальный исходный текст конструкцию switch или последовательность выражений if . В обоих случаях код выглядит одинаково, поскольку оба выражения используют множество инструкций cmp и je или jne.

Таблица переходов

Следующий пример ассемблерного кода часто можно встретить в больших смежных выражениях switch. Мы добавим case 4 и инструкцию по умолчанию.

Код на Си

#include <stdio.h>int main() {    int i = 3;    switch(i) {        case 1:            printf("CASE_1 i = %d", i+4);            break;        case 2:            printf("CASE_2 i = %d", i+9);            break;        case 3:            printf("CASE_3 i = %d", i+14);            break;        case 4:            printf("CASE_3 i = %d", i+19);            break;        default:            break;    }    return 0;}

Дизассемблированный вариант в radare2

1605793648917.png1605793648917.png1605793656018.png1605793656018.png

1 - инициализации переменной var_4h (i = 3)
2 - выполнения инструкций (add, printf)

Вот этот дизасcемблированный код довольно сложно быстро отличить от if и вообще понять что и как тут. В режиме графов всё будет более понятно.

Режим графов

1605793750977.png1605793750977.png1605793673414.png1605793673414.png1605793684884.png1605793684884.png1605793691266.png1605793691266.png

Режим графов - ваш друг в дизасcемблировании :)

На этом всё. Рекомендую попробовать самому написать программы на Си, скомпилировать и изучить дизасcемблированный код. Практика и ещё раз практика!

Спасибо за внимание. Не болейте.

Подробнее..

Перевод Как я написал интро 4K на Rust и оно победило

07.07.2020 12:09:02 | Автор: admin
Недавно я написал своё первое интро 4K на Rust и представил его на Nova 2020, где оно заняло первое место в конкурсе New School Intro Competition. Написать интро 4K довольно сложно. Это требует знания многих различных областей. Здесь я сосредоточусь на методах, как максимально сократить код Rust.


Можете просмотреть демо-версию на Youtube, скачать исполняемый файл на Pouet или получить исходный код с Github.

Интро 4K это демо, в которой вся программа (включая любые данные) занимает 4096 байта или меньше, поэтому важно, чтобы код был максимально эффективным. Rust имеет некоторую репутацию создания раздутых исполняемых файлов, поэтому я хотел выяснить, можно ли написать на нём эффективный и лаконичный код.

Конфигурация


Всё интро написано на комбинации Rust и glsl. Glsl используется для рендеринга, но Rust делает всё остальное: создание мира, управление камерой и объектами, создание инструментов, воспроизведение музыки и т.д.

В коде есть зависимости от некоторых функций, которые ещё не включены в стабильный Rust, поэтому я использую набор инструментов Nightly Rust. Чтобы установить и использовать этот набор по умолчанию, нужно запустить следующие команды rustup:

rustup toolchain install nightlyrustup default nightly

Я использую crinkler для сжатия объектного файла, сгенерированного компилятором Rust.

Я также использовал shader minifier для препроцессинга шейдера glsl, чтобы сделать его меньше и удобнее для crinkler. Shader minifier не поддерживает вывод в .rs, так что я брал необработанную выдачу и вручную копировал её в свой файл shader.rs (задним умом ясно, что нужно было как-то автоматизировать этот этап. Или даже написать пул-реквест для shader minifier).

Отправной точкой послужила моё прошлое интро 4K на Rust, которое тогда мне казалось довольно лаконичным. В той статье также более подробная информация о настройке файла toml и о том, как использовать xargo для компиляции крошечного бинарника.

Оптимизация дизайна программы для уменьшения кода


Многие из наиболее эффективных оптимизаций размера никак не назовёшь умными хаками. Это результат переосмысления дизайна.

В моём первоначальном проекте одна часть кода создавала мир, включая размещение сфер, а другая часть отвечала за перемещение сфер. В какой-то момент я понял, что код размещения и код перемещения сферы делают очень похожие вещи, и можно объединить их в одну значительно более сложную функцию, которая делает и то, и другое. К сожалению, такие оптимизации делают код менее элегантным и читабельным.

Анализ ассемблерного кода


В какой-то момент придётся посмотреть на скомпилированный ассемблер и разобраться, во что компилируется код и какие оптимизации размера стоят того. В компиляторе Rust есть очень полезная опция --emit=asm для вывода ассемблерного кода. Следующая команда создаёт файл ассемблера .s:

xargo rustc --release --target i686-pc-windows-msvc -- --emit=asm

Не обязательно быть экспертом в ассемблере, чтобы извлечь выгоду из изучения выходных данных ассемблера, но определённо лучше иметь базовое понимание синтаксиса. Опция opt-level = "z заставляет компилятор максимально оптимизировать код для наименьшего размера. После этого немного сложнее выяснить, какая часть кода ассемблера соответствует какой части кода Rust.

Я обнаружил, что компилятор Rust может быть удивительно хорош в минимизации, удалении неиспользуемого кода и ненужных параметров. Он также делает некоторые странные вещи, поэтому очень важно время от времени изучать результат на ассемблере.

Дополнительные функции


Я работал с двумя версиями кода. Одна протоколирует процесс и позволяет зрителю манипулировать камерой для создания интересных траекторий. Rust позволяет определить функции для этих дополнительных действий. В файле toml есть раздел [features], который позволяет объявлять доступные функции и их зависимости. В toml соего интро 4K есть следующий раздел:

[features]logger = []fullscreen = []

Ни одна из дополнительных функций не имеет зависимостей, поэтому они эффективно работают как флаги условной компиляции. Условным блокам кода предшествует оператор #[cfg(feature)]. Использование функций само по себе не делает код меньше, но сильно упрощает процесс разработки, когда вы легко переключаетесь между различными наборами функций.

        #[cfg(feature = "fullscreen")]        {            // Этот код компилируется только в том случае, если выбран полноэкранный режим        }        #[cfg(not(feature = "fullscreen"))]        {            // Этот код компилируется только в том случае, если полноэкранный режим не выбран        }

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

Одним из основных применений функций было включение лога и проверки ошибок для отладочной сборки. Загрузка кода и компиляция шейдера glsl часто завершались неудачно, и без полезных сообщений об ошибках было бы чрезвычайно трудно находить проблемы.

Использование get_unchecked


При размещении кода внутри блока unsafe{} я вроде как предполагал, что все проверки безопасности будут отключены, но это не так. Там по-прежнему выполняются все обычные проверки, и они дорого обходятся.

По умолчанию range проверяет все обращения к массиву. Возьмите следующий код Rust:

    delay_counter = sequence[ play_pos ];

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

Преобразуем код следующим образом:

    delay_counter = *sequence.get_unchecked( play_pos );

Это говорит компилятору, чтобы он не выполнял никаких проверок диапазона и просто искал по таблице. Это явно опасная операция и поэтому её можно выполнять только в пределах кода unsafe.

Более эффективные циклы


Изначально все мои циклы использовали выполнялись идиоматически как положено в Rust, используя синтаксис for x in 0..10. Я предполагал, что он будет скомпилирован в максимально плотный цикл. Удивительно, но это не так. Простейший случай:

for x in 0..10 {    // do code}

будет скомпилирован в ассемблерный код, который делает следующее:

    setup loop variableloop:    проверить условие цикла        если цикл закончен, перейти в end    // выполнить код внутри цикла    безусловно перейти в loopend:

тогда как следующий код

let x = 0;loop{    // do code    x += 1;    if i == 10 {        break;    }}

непосредственно компилируется в:

    setup loop variableloop:    // выполнить код внутри цикла    проверить условие цикла        если цикл не закончен, перейти в loopend:

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

Другая, гораздо более трудная для понимания проблема с идиоматическим циклом Rust заключается в том, что в некоторых случаях компилятор добавлял некоторый дополнительный код настройки итератора, который действительно раздувал код. Я так и не понял, что вызывает эту дополнительную настройку итератора, поскольку всегда было тривиально заменить конструкции for {} конструкцией loop{}.

Использование векторных инструкций


Я много времени потратил на оптимизацию кода glsl, и одна из лучших оптимизаций (которая ещё обычно и ускоряет работу кода) состоит в одновременной работе со всем вектором, а не с каждым компонентом по очереди.

Например, код трассировки лучей использует быстрый алгоритм обхода сетки для проверки, какие части карты посещает каждый луч. Исходный алгоритм рассматривает каждую ось отдельно, но можно переписать его так, чтобы он рассматривал все оси одновременно и не нуждался в каких-либо ветвях. Rust на самом деле не имеет собственного векторного типа, такого как glsl, но вы можете использовать внутренние компоненты, чтобы указать использовать инструкции SIMD.

Чтобы использовать встроенные функции, я бы преобразовал следующий код

        global_spheres[ CAMERA_ROT_IDX ][ 0 ] += camera_rot_speed[ 0 ]*camera_speed;        global_spheres[ CAMERA_ROT_IDX ][ 1 ] += camera_rot_speed[ 1 ]*camera_speed;        global_spheres[ CAMERA_ROT_IDX ][ 2 ] += camera_rot_speed[ 2 ]*camera_speed;

в такое:

        let mut dst:x86::__m128 = core::arch::x86::_mm_load_ps(global_spheres[ CAMERA_ROT_IDX ].as_mut_ptr());        let mut src:x86::__m128 = core::arch::x86::_mm_load_ps(camera_rot_speed.as_mut_ptr());        dst = core::arch::x86::_mm_add_ps( dst, src);        core::arch::x86::_mm_store_ss( (&mut global_spheres[ CAMERA_ROT_IDX ]).as_mut_ptr(), dst );

что будет немного меньше по размеру (и гораздо менее читабельно). К сожалению, по какой-то причине это сломало отладочную сборку, хотя прекрасно работало в релизной. Ясно, что здесь проблема с моим знанием внутренних средств Rust, а не с самим языком. На это стоит потратить больше времени при подготовке следующего 4K-интро, поскольку сокращение объёма кода было значительным.

Использование OpenGL


Есть множество стандартных крейтов Rust для загрузки функций OpenGL, но по умолчанию все они загружают очень большой набор функций. Каждая загруженная функция занимает некоторое пространство, потому что загрузчик должен знать её имя. Crinkler очень хорошо сжимает такого рода код, но он не в состоянии полностью избавиться от оверхеда, поэтому пришлось создать свою собственную версию gl.rs, включающую только нужные функции OpenGL.

Заключение


Главная цель состояла в том, чтобы написать конкурентоспособное корректное интро 4K и доказать, что язык Rust пригоден для демосцены и для сценариев, где каждый байт имеет значение и вам действительно нужен низкоуровневый контроль. Как правило, в этой области рассматривали только ассемблер и C. Дополнительная цель состояла в максимальном использовании идиоматического Rust.

Мне кажется, что я довольно успешно справился с первой задачей. Ни разу не возникало ощущения, что Rust каким-то образом сдерживает меня или что я жертвую производительностью или функциями, потому что использую Rust, а не C.

Со второй задачей я справился менее успешно. Слишком много небезопасного кода, который на самом деле не должен быть там. unsafe несёт разрушающий эффект; очень легко его использовать, чтобы быстро что-то выполнить (например, используя изменяемые статические переменные), но как только появляется небезопасный код, он порождает ещё более небезопасный код, и внезапно он оказывается повсюду. В будущем я буду гораздо осторожнее использовать unsafe только тогда, когда действительно нет альтернативы.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru