Для решения этого вопроса необходимо понять три принципа.
Ассемблер должен иметь возможность определить правильную длину.
Хотя в синтаксисе Intel не используется суффикс размера, такой как AT & T синтаксис , ассемблеру по-прежнему нужен способ определить размер операндов.
Неоднозначная инструкция mov [var], 1
записывается как movl $1, var
в синтаксисе AT & T, если размер хранилища 32-разрядный (обратите внимание на суффикс l
), поэтому легко определить размер непосредственного операнда.
Ассемблеру, который принимает синтаксис Intel, нужен способ вывести этот размер, есть четыре широко используемых параметра:
- Выводится из другого операнда.
Это тот случай, когда используется регистр, например.
Например. mov [var], dx
- 16-битный магазин.
- Это указано явно.
mov WORD [var], dx
Ассемблерам MASM-синтаксиса требуется PTR
после размера, поскольку их спецификаторы размера разрешены только для операндов памяти, а не для непосредственных объектов или где-либо еще.
Это форма, которую я предпочитаю, потому что она понятна, она выделяется, и она менее подвержена ошибкам (mov WORD [var], edx
недопустимо).
Выводится из контекста.
var db 0
mov [var], 1 ; MASM/TASM only. associate sizes with labels
Ассемблеры MASM-синтаксиса могут сделать вывод, что, поскольку var
объявлен с db
, его размер 8-битный, как и хранилище (по умолчанию).
Эта форма мне не нравится, потому что она затрудняет чтение кода (одна хорошая вещь в сборке - это «локальность» семантики инструкций) и смешивает высокоуровневые понятия, такие как типы, с низкоуровневыми понятиями, такими как store. размеры. Вот почему синтаксис NASM не поддерживает магическое / нелокальное сопоставление размеров .
- В большинстве случаев есть только один правильный размер
Это относится к push
, ветвям и всем инструкциям, где размер их операнда зависит от модели памяти или размера кода.
Используемый фактический размер может быть переопределен для некоторых инструкций, но по умолчанию это разумный выбор. (например, push word 123
против push 123
)
Короче говоря, должен быть способом, которым ассемблер может указать размер, иначе он отклонит код. (Или некоторые некачественные ассемблеры, такие как emu8086, имеют размер операнда по умолчанию для неоднозначных случаев.)
Если вы смотрите на дизассемблированный код, дизассемблеры обычно принимают безопасную сторону и всегда указывают размер явно.
Если нет, вы должны прибегнуть к ручной проверке кода операции, если дизассемблер не покажет коды операции, пришло время изменить его.
У дизассемблера нет проблем с определением размера операнда, так как двоичный код, который он разбирает, выполняется центральным процессором, а коды операций инструкций кодируют размер операнда.
Язык C преднамеренно не соответствует тому, как типы C отображаются в число битов
Не бесполезно пытаться определить тип переменной по разборке, но нужно учитывать и платформу, а не только архитектуру.
Основные используемые модели обсуждаются здесь :
Datatype LP64 ILP64 LLP64 ILP32 LP32
char 8 8 8 8 8
short 16 16 16 16 16
_int32 32
int 32 64 32 32 16
long 64 64 32 32 32
long long 64 [64]
pointer 64 64 64 32 32
Windows на x86_64 использует LLP64. Другие ОС на x86-64 обычно используют x86-64 System V ABI, модель LP64.
Сборка не имеет типов, и программисты могут использовать это
Даже компиляторы могут использовать это .
В случае, если связанная переменная bar
типа long long
(64-разрядная) имеет значение OR, равное 1, clang
резервирует префикс REX путем ORing только младшего байта. Это приводит к задержке переадресации магазина, если переменная сразу же перезагружается сразу с двумя загрузками dword или одним qword, поэтому это, вероятно, не лучший выбор, особенно в 32-битном режиме, где or dword [bar], 1
имеет тот же размер и, вероятно, будет перезагружен как две 32-битные половины.
Если бы кто-то неосторожно взглянул на разобранный код, он мог бы заключить, что bar
является 8-битным.
Подобные уловки, когда переменная или объект доступны частично, являются общими.
Чтобы правильно угадать размер переменной, требуется немного опыта.
Например, элементы конструкций обычно дополняются, поэтому между ними есть неиспользуемое пространство, которое может заставить неопытного пользователя думать, что каждый элемент больше, чем он есть.
У стека есть точные требования к выравниванию, которые также могут расширить параметры размера .
Практическое правило заключается в том, что компиляторы обычно предпочитают, чтобы стек был 16-байтовым и выровняли все переменные. Несколько узких переменных упакованы в одно слово . При передаче аргументов функции через стек каждый дополняется до 32- или 64-битного, но это не относится к расположению локальных элементов в стеке.
Чтобы окончательно ответить на ваш вопрос
Да, из первого фрагмента кода можно предположить, что значение arg_0
имеет ширину 16 бит.
Обратите внимание, что поскольку в стек передается функция arg, она на самом деле 32-разрядная, но старшие 16 бит не используются.
Если mov ecx, [ebp+arg_0]
появилось позже в коде, чем вам бы пришлось пересмотреть свое предположение о размере значения arg_0
, оно, безусловно, по крайней мере 32-разрядное.
Маловероятно, что он 64-битный (64-битный тип редко встречается в 32-битном коде, мы можем сделать эту ставку), поэтому мы можем заключить, что он 32-битный.
Очевидно, что первый фрагмент был одним из тех трюков, которые используют только часть переменной.
Вот как вы работаете с реверс-инжинирингом размера переменной, вы делаете предположение, проверяете, соответствует ли он остальному коду, и возвращайтесь к нему, если нет, повторяйте.
Со временем вы будете делать в основном хорошие предположения, которые вообще не нуждаются в пересмотре.