gcc удаляет встроенный код ассемблера

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

test.c

int main(void) { goto exit; handler: __asm__ __volatile__("jmp 0x0"); exit: return 0; } 

Разборка main()

  0x08048404 : push ebp 0x08048405 : mov ebp,esp 0x08048407 : nop # <-- This is all whats left of my jmp. 0x08048408 : mov eax,0x0 0x0804840d : pop ebp 0x0804840e : ret 

Параметры компилятора

Никаких оптимизаций не было, просто gcc -m32 -o test test.c ( -m32 потому что я на 64-битной машине).

Как я могу остановить это поведение?

Изменить: желательно с помощью параметров компилятора, а не путем изменения кода.

Обновление 2012/6/18

Просто подумав об этом, можно положить goto exit в блок asm, а это значит, что нужно изменить только одну строку кода:

 int main(void) { __asm__ ("jmp exit"); handler: __asm__ __volatile__("jmp $0x0"); exit: return 0; } 

Это значительно более чистое, чем мое другое решение ниже (и, возможно, более приятное, чем текущее значение @ ugoren).


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

 int main (void) { int x = 0; __asm__ __volatile__ ("" : "=r"(x)); // compiler can't tell what the value of x is now, but it's always 0 if (x) { handler: __asm__ __volatile__ ("jmp $0x0"); } return 0; } 

Даже при -O3 сохраняется jmp :

  testl %eax, %eax je .L2 .L3: jmp $0x0 .L2: xorl %eax, %eax ret 

(Это кажется действительно изворотливым, поэтому я надеюсь, что есть лучший способ сделать это. Редактирование просто ставит volatile перед работами x поэтому не нужно делать встроенную азбуку asm.)

Похоже, это так, как есть. Когда gcc видит, что код внутри функции недоступен, он удаляет его. Другие компиляторы могут отличаться.
В gcc на ранней стадии компиляции строится «граф streamа управления» – график «базовых блоков», каждый из которых свободен от условий, связанных ветвями. При испускании фактического кода части графика, которые недоступны из корня, отбрасываются.
Это не является частью фазы оптимизации и поэтому не зависит от параметров компиляции.

Поэтому любое решение предполагает, что gcc считает, что код доступен.

Мое предложение:

Вместо того, чтобы помещать код сборки в недостижимое место (где GCC может удалить его), вы можете поместить его в доступное место и пропустить проблемную инструкцию:

 int main(void) { goto exit; exit: __asm__ __volatile__ ( "jmp 1f\n" "jmp $0x0\n" "1:\n" ); return 0; } 

Также см. Эту тему об этой проблеме .

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


Комментарий о принятом ответе

В принятом ответе есть оригинал, который предлагает это решение:

 int main(void) { __asm__ ("jmp exit"); handler: __asm__ __volatile__("jmp $0x0"); exit: return 0; } 

Сначала jmp $0x0 должен быть jmp 0x0 . Во-вторых, метки C обычно переводятся на локальные метки. jmp exit фактически не перескакивает на exit метки в функции C , он переходит к функции exit в библиотеке C, эффективно минуя return 0 в нижней части main . Используя Godbolt с GCC 4.6.4, мы получаем этот неоптимизированный результат (я обрезал метки, которые нам не нужны):

 main: pushl %ebp movl %esp, %ebp jmp exit jmp 0x0 .L3: movl $0, %eax popl %ebp ret 

.L3 на самом деле является локальной меткой для exit . Вы не найдете ярлык exit в сгенерированной сборке. Он может компилировать и связывать, если библиотека C присутствует. Не используйте C локальные метки goto в встроенной сборке, как это.


Использовать asm goto как решение

Начиная с GCC 4.5 (OP использует 4.6.x) существует поддержка расширенных шаблонов сборки asm goto . asm goto позволяет вам указать цели перехода, которые может использовать встроенная assembly:

6.45.2.7 Goto Labels

asm goto позволяет ассемблеру перейти к одной или нескольким меткам С. Раздел GotoLabels в инструкции asm goto содержит список разделенных запятыми всех меток C, к которым может перейти код ассемблера. GCC предполагает, что выполнение asm попадает в следующий оператор (если это не так, рассмотрите возможность использования __builtin_unreachable intrinsic после оператора asm). Оптимизация asm goto может быть улучшена с использованием атрибутов горячих и холодных меток (см. Атрибуты метки).

Оператор asm goto не может иметь выходы. Это связано с внутренним ограничением компилятора: команды управления передачей не могут иметь выходы. Если код ассемблера ничего не модифицирует, используйте «клобук» памяти, чтобы заставить оптимизаторов сбросить все значения регистров в память и перезагрузить их, если необходимо после оператора asm.

Также обратите внимание, что оператор asm goto всегда неявно считается изменчивым.

Чтобы ссылаться на метку в шаблоне ассемблера, прикрепите ее к «% l» (нижний регистр «L»), за которой следует его (нулевая) позиция в GotoLabels и количество входных операндов. Например, если в asm есть три входа и ссылки на две метки, обратитесь к первой метке как «% l3», а вторая как «% l4»).

В качестве альтернативы вы можете ссылаться на метки, используя фактическое имя метки C, заключенное в скобки. Например, чтобы ссылаться на метку с именем carry, вы можете использовать «% l [carry]». При использовании этого подхода метка должна быть указана в разделе GotoLabels.

Код можно написать следующим образом:

 int main(void) { __asm__ goto ("jmp %l[exit]" :::: exit); handler: __asm__ __volatile__("jmp 0x0"); exit: return 0; } 

Мы можем использовать asm goto . Я предпочитаю __asm__ над asm так как он не будет __asm__ предупреждения, если компилируется с -ansi или -std=? опции. После клиберов вы можете указать цели перехода, которые может использовать встроенная assembly. C фактически не знает, прыгаем мы или нет, поскольку GCC не анализирует фактический код в шаблоне встроенной сборки. Он не может удалить этот прыжок, и он не может предположить, что происходит после мертвого кода. Используя Godbolt с GCC 4.6.4, неоптимизированный код (обрезанный) выглядит так:

 main: pushl %ebp movl %esp, %ebp jmp .L2 # <------ this is the goto exit jmp 0x0 .L2: # <------ exit label movl $0, %eax popl %ebp ret 

Godbolt с выходом GCC 4.6.4 по- прежнему выглядит корректно и выглядит следующим образом:

 main: jmp .L2 # <------ this is the goto exit jmp 0x0 .L2: # <------ exit label xorl %eax, %eax ret 

Этот механизм также должен работать независимо от того, включена ли вы оптимизация или нет, и не имеет значения, компилируете ли вы для 64-разрядных или 32-разрядных целей x86.


Другие наблюдения

  • Если в расширенном встроенном шаблоне сборки нет ограничений вывода, оператор asm неявно нестабилен. Линия

     __asm__ __volatile__("jmp 0x0"); 

    Может быть написано как:

     __asm__ ("jmp 0x0"); 
  • Операторы asm goto считаются неявно изменчивыми. Они также не требуют volatile модификатора.

Будет ли это работать, сделайте так, чтобы gcc не мог знать его недостижимый

 int main(void) { volatile int y = 1; if (y) goto exit; handler: __asm__ __volatile__("jmp 0x0"); exit: return 0; } 

Если компилятор считает, что он может обмануть вас, просто обманите назад: (только GCC)

 int main(void) { { /* Place this code anywhere in the same function, where * control flow is known to still be active (such as at the start) */ extern volatile unsigned int some_undefined_symbol; __asm__ __volatile__(".pushsection .discard" : : : "memory"); if (some_undefined_symbol) goto handler; __asm__ __volatile__(".popsection" : : : "memory"); } goto exit; handler: __asm__ __volatile__("jmp 0x0"); exit: return 0; } 

Это решение не добавит дополнительных накладных расходов для бессмысленных инструкций, хотя работает только для GCC при использовании с AS (как и по умолчанию).

Объяснение: .pushsection переключает текстовый вывод компилятора в другой раздел, в этом случае .discard (который удаляется при связывании по умолчанию). "memory" Клобум "memory" позволяет GCC пытаться переместить другой текст в пределах раздела, который будет отброшен. Однако GCC не понимает (и никогда не может, потому что __asm__ s __volatile__ ), что все, что происходит между двумя утверждениями, будет отброшено.

Что касается some_undefined_symbol , это буквально просто любой символ, который никогда не определяется (или фактически определен, это не имеет значения). И так как раздел кода, использующий его, будет отброшен во время связывания, он также не приведет к каким-либо нерешенным-справочным ошибкам.

Наконец, условный переход на ярлык, который вы хотите создать, выглядит так, как если бы он был доступен, делает именно это. Помимо того факта, что он вообще не появляется в выходном двоичном формате, GCC понимает, что он ничего не знает о some_undefined_symbol , то есть у него нет выбора, кроме как предположить, что обе ветви if достижимы, что означает, что до он обеспокоен тем, что stream управления может продолжаться как путем выхода goto exit , так и путем goto exit к handler (хотя не будет никакого кода, который мог бы даже это сделать)

Однако будьте осторожны при включении сборки мусора в ваших компоновщиках ld --gc-sections (по умолчанию он отключен), поскольку в противном случае может возникнуть идея избавиться от все еще неиспользуемой метки независимо.

EDIT: Забудьте все это. Просто сделайте это:

 int main(void) { __asm__ __volatile__ goto("" : : : : handler); goto exit; handler: __asm__ __volatile__("jmp 0x0"); exit: return 0; } 

Я никогда не слышал о том, как предотвратить gcc от удаления недостижимого кода; кажется, что независимо от того, что вы делаете, как только gcc обнаруживает недостижимый код, он всегда удаляет его (используйте параметр gcc -Wunreachable-code чтобы увидеть, что он считает недостижимым).

Тем не менее, вы все равно можете поставить этот код в статическую функцию, и он не будет оптимизирован:

 static int func() { __asm__ __volatile__("jmp $0x0"); } int main(void) { goto exit; handler: func(); exit: return 0; } 

PS
Это решение особенно удобно, если вы хотите избежать избыточности кода при имплантации одного и того же кода кода «обработчик» в более чем одном месте в исходном коде.

gcc может дублировать операторы asm внутри функций и удалять их во время оптимизации (даже при -O0), поэтому это никогда не будет работать надежно.

один из способов сделать это надежно – использовать глобальный оператор asm (т. е. оператор asm вне любой функции). gcc скопирует это прямо на выход, и вы можете использовать глобальные метки без каких-либо проблем.