Другие ответы касаются только NOP, который на самом деле выполняется в какой-то момент - он используется довольно часто, но это не единственное использование NOP.
Неисполнение NOP также очень полезно при написании кода , который может быть исправлен - в основном, вы будете подушечка функция с несколько НОПОМ после на RET
(или аналогичной инструкции). Когда вам нужно пропатчить исполняемый файл, вы можете легко добавить больше кода в функцию, начиная с оригинала RET
и используя столько NOP, сколько вам нужно (например, для длинных переходов или даже встроенного кода) и заканчивая другим RET
.
В этом случае использования noöne никогда не ожидает NOP
выполнения. Единственное, что нужно, это разрешить исправление исполняемого файла - в теоретическом незаполненном исполняемом файле вам фактически нужно изменить код самой функции (иногда она может соответствовать исходным границам, но довольно часто вам все равно потребуется переход ) - это намного сложнее, особенно если учесть написанную вручную сборку или оптимизирующий компилятор; Вы должны уважать переходы и подобные конструкции, которые могли указывать на какой-то важный фрагмент кода. В общем, довольно сложно.
Конечно, это было гораздо интенсивнее использовать в старые времена, когда было полезно делать такие патчи, как эти маленькие и онлайн . Сегодня вы просто раздадите перекомпилированный бинарный файл и покончите с этим. Есть еще некоторые, кто использует исправления NOP (выполняются или нет, и не всегда буквальные NOP
s - например, Windows использует MOV EDI, EDI
для оперативного исправления - это тот тип, где вы можете обновлять системную библиотеку, пока система фактически работает, без необходимости перезапусков).
Итак, последний вопрос: почему есть специальная инструкция для чего-то, что на самом деле ничего не делает?
- Это актуальная инструкция, важная при отладке или сборке кода вручную. Инструкции вроде
MOV AX, AX
будут делать то же самое, но не так ясно обозначают намерение.
- Заполнение - это «код», предназначенный только для улучшения общей производительности кода, который зависит от выравнивания. Это никогда не предназначалось для выполнения. Некоторые отладчики просто скрывают дополнительные NOP при их разборке.
- Это дает больше места для оптимизации компиляторов - по-прежнему используется шаблон, состоящий в том, что у вас есть два этапа компиляции, первый из которых довольно прост и производит много ненужного ассемблерного кода, а второй очищает, перезаписывает адресные ссылки и удаляет посторонние инструкции. Это часто встречается и в JIT-скомпилированных языках - как в .NET IL, так и в JVM-байт-коде
NOP
достаточно много; у фактического скомпилированного ассемблерного кода таких больше нет. Следует отметить, что это не x86- NOP
е.
- Это облегчает онлайн-отладку как для чтения (предварительно обнуленная память будет всем
NOP
, что облегчает чтение разборки), так и для оперативного исправления (хотя я на самом деле предпочитаю Edit и Continue в Visual Studio: P).
Для выполнения NOP, конечно, есть еще несколько моментов:
- Конечно, производительность - это не то, почему это было в 8085 году, но даже у 80486 уже было конвейерное выполнение команд, что делает «бездействие» немного хитрее.
- Как видно
MOV EDI, EDI
, есть и другие эффективные NOP, чем буквальные NOP
. MOV EDI, EDI
имеет лучшую производительность как 2-байтовый NOP на x86. Если вы использовали два NOP
s, это будет две инструкции для выполнения.
РЕДАКТИРОВАТЬ:
На самом деле, обсуждение с @DmitryGrigoryev заставило меня задуматься об этом немного больше, и я думаю, что это ценное дополнение к этому вопросу / ответу, поэтому позвольте мне добавить несколько дополнительных моментов:
Во-первых, очевидно, зачем нужна инструкция, которая делает что-то подобное mov ax, ax
? Например, давайте рассмотрим случай машинного кода 8086 (старше, чем даже машинный код 386):
- Там есть специальная инструкция NOP с кодом операции
0x90
. Это все еще время, когда многие люди писали на ассамблее. Таким образом, даже если бы не было специальной NOP
инструкции, NOP
ключевое слово (псевдоним / мнемоника) все равно было бы полезно и соответствовало бы этому.
- Такие инструкции, как на
MOV
самом деле, отображаются на множество различных кодов операций, потому что это экономит время и пространство - например, mov al, 42
«переместить непосредственный байт в al
регистр», что означает 0xB02A
( 0xB0
код операции, 0x2A
являющийся «немедленным» аргументом). Так что это занимает два байта.
- Там нет быстрого кода операции
mov al, al
(поскольку это глупо, в принципе), поэтому вам придется использовать mov al, rmb
перегрузку (rmb - «регистр или память»). Это на самом деле занимает три байта. (хотя, вероятно, mov rb, rmb
вместо этого он использовал бы менее конкретный , который должен занимать только два байта mov al, al
- байт аргумента используется для указания как исходного, так и целевого регистра; теперь вы знаете, почему у 8086 было только 8 регистров: D). Сравните с NOP
, который является однобайтовой инструкцией! Это экономит память и время, поскольку чтение памяти в 8086 все еще было довольно дорогим - не говоря уже о загрузке этой программы с ленты, с дискеты или чего-то другого, конечно.
Так откуда же xchg ax, ax
взяться? Вам просто нужно посмотреть коды операций других xhcg
инструкций. Вы увидите 0x86
, 0x87
и , наконец, 0x91
- 0x97
. Таким образом, nop
это 0x90
выглядит довольно неплохо xchg ax, ax
(что, опять же, не является xchg
«перегрузкой» - вам придется использовать xchg rb, rmb
два байта). И на самом деле, я почти уверен, что это был хороший побочный эффект 0x90-0x97
микроархитектуры того времени - если я правильно помню, было легко сопоставить весь диапазон с «xchg, действующим над регистрами ax
и ax
- di
» ( операнд был симметричным, это дало вам полный диапазон, включая nop xchg ax, ax
; обратите внимание, что порядок ax, cx, dx, bx, sp, bp, si, di
- bx
после dx
,ax
; помните, что имена регистров являются мнемониками, а не упорядоченными именами - аккумулятор, счетчик, данные, база, указатель стека, указатель базы, указатель источника, указатель назначения). Тот же подход использовался и для других операндов, например, для mov someRegister, immediate
множества. В некотором смысле, вы можете думать об этом, как будто код операции на самом деле не был полным байтом - последние несколько битов являются «аргументом» для «настоящего» операнда.
Все это говорит о том, что на x86 nop
можно считать настоящей инструкцией или нет. Оригинальная микроархитектура действительно рассматривала его как вариант, xchg
если я правильно помню, но на самом деле это было названо nop
в спецификации. И поскольку в xchg ax, ax
действительности это не имеет смысла в качестве инструкции, вы можете увидеть, как разработчики 8086 сэкономили на транзисторах и путях при декодировании команд, используя тот факт, что он 0x90
естественным образом отображается на что-то, что является полностью «неопрятным».
С другой стороны, i8051 имеет полностью встроенный код операции для nop
- 0x00
. Вроде практично. Конструкция команды в основном использует высокий полубайт для операции и низкий полубайт для выбора операндов - например, add a
есть 0x2Y
, и 0xX8
означает «прямой регистр 0», то 0x28
есть add a, r0
. Экономит много на силиконе :)
Я все еще мог бы продолжить, так как дизайн процессора (не говоря уже о дизайне компилятора и дизайне языка) является довольно широкой темой, но я думаю, что я показал много разных точек зрения, которые вошли в дизайн довольно хорошо, как есть.