Всегда ли инструкция после RET та же, что после CALL? - PullRequest
4 голосов
/ 10 марта 2012

В программе C с хорошим поведением должен ли оператор return (RET) всегда возвращаться к инструкции, следующей за инструкцией CALL?Я знаю, что это по умолчанию, но я хотел бы проверить, если кто-нибудь знает или помнит подлинные примеры случаев, когда этот стандарт не применяется (общая оптимизация компилятора или другие вещи ...).Кто-то сказал мне, что это может произойти с указателем на функцию (указатель на функцию поместит значение в стек вместо CALL ... Я искал его, но нигде не видел объяснения).

Позвольте мне попытаться лучше объяснить мой вопрос.Я знаю, что мы можем использовать другие структуры для изменения потока выполнения (включая манипулирование стеком) ... Я понимаю, что если мы изменим адрес возврата, записанный в стеке, поток выполнения изменится на адрес, который был записан в стеке.Что мне нужно знать: есть ли какая-нибудь необычная ситуация выполнения, когда следующая инструкция не та, которая следует за CALL?Я хочу быть уверен, что этого не произойдет, если не произойдет что-то неожиданное (например, нарушение доступа к памяти, которое приведет к структурированному обработчику исключений).

Меня беспокоит вопрос о том, являются ли коммерческие прикладные программыв общем, ВСЕГДА следуйте упомянутой схеме.Обратите внимание, что в этом случае у меня есть фиксация для исключений (важно знать, существуют ли они в этом случае, для исследовательского проекта, который я разрабатываю в дисциплине программы магистра наук).Я знаю, например, что компилятор может иногда изменять RET на JMP (оптимизация хвостового вызова).Я хотел бы знать, может ли что-то подобное изменить порядок команды, выполняемой после RET, и, главным образом, будет ли CALL всегда перед инструкцией, выполняемой после RET.

Ответы [ 6 ]

1 голос
/ 10 марта 2012

"С хорошим поведением" C-программа может быть преобразована компилятором в программу, которая не следует этому шаблону. Например, по причинам запутанности код может использовать комбинацию push / ret вместо jmp.

0 голосов
/ 10 марта 2012

Может быть.На некоторых процессорах есть нечто, называемое «временной интервал задержки» (иногда два), которые являются инструкциями, следующими непосредственно за инструкциями ветвления (включая CALL), которые выполняются так, как если бы они находились в цели ветвления.Эта кажущаяся ерунда была добавлена ​​для повышения производительности, так как средство предварительной выборки команд довольно часто выбиралось перед инструкцией ветвления к тому моменту, когда она понимает, что ветвление существует.Адрес, выдаваемый CALL в качестве адреса возврата, равен , а не адрес, следующий за CALL, если есть инструкции слота задержки, адрес возврата - это адрес, следующий за инструкциями слота задержки.

http://en.wikipedia.org/wiki/Delay_slot

Эта введенная сложность в архитектуре набора инструкций (ISA) для этой машины, например, что происходит, если вы помещаете ветви в интервалы задержки, что происходит, если инструкция в интервале задержки вызывает ошибку?Что произойдет, если будет ловушка (например, одношаговая ловушка)?Вы можете видеть, что это становится беспорядочным ... но у удивительного числа более старых процессоров RISC есть это, как MIPS, SPARC и PA-RISC.

0 голосов
/ 10 марта 2012

Теоретически, компилятор может, учитывая следующий код:

return f(), g();

генерирует сборку в соответствии с:

push $g
jmp f
0 голосов
/ 10 марта 2012

Адрес подпрограммы CALL эквивалентен
PUSH адрес следующей инструкции + Адрес подпрограммы JMP .

В то же время, PUSH-адрес почти эквивалентен
SUB xSP, размер указателя + MOV [xSP], адрес .

SUBxSP, размер указателя можно заменить на PUSH .

RET почти эквивалентно
JMP [xSP] с последующим ADD xSP, адрес указателя в том месте, куда ведет JMP.

и ADD xSP, адрес указателя можно заменить на POP .

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

Хотя это и несколько неверно, но невозможно придумать много более странных передач управления, используяинструкции и методы, специфичные для платформы (ЦП и ОС).

Вы можете использовать IRET вместо CALL и RET для передачи управленияпри условии, что вы поместили соответствующий материал в стек для инструкции.

Windows Structured Exception Handling можно использовать таким образом, чтобы инструкция, вызывающая исключение ЦП (например, деление на 0, ошибка страницы и т. д.), отклоняласьвыполнение к вашему обработчику исключений, и оттуда управление может быть передано либо обратно той же инструкции, либо следующему, либо следующему обработчику исключений, либо в любое место.И большинство инструкций x86 могут вызывать исключения CPU.

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

Весьма необычно видеть что-то в кодеили вот так:

...
CALL A
A: JMP B
db "some data", 0
B: CALL C ; effectively call C with a pointer to "some data" as a parameter.
...

C:
; extracts the location of "some data" from the stack and uses it.
...
RET

Здесь первый вызов - это не подпрограмма, это просто способ поместить в стек адрес данных, застрявших в середине кода.

Это, вероятно, написал бы программист, а не компилятор.Но я могу ошибаться.

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

0 голосов
/ 10 марта 2012

В программе C с хорошим поведением должен ли оператор return (RET) всегда возвращаться к инструкции, следующей за инструкцией CALL?

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

Я думаю, что для компилятора, нацеленного на x86, было бы очень необычно подтасовывать вещи, поэтому инструкция ret, соответствующая инструкции return, шла куда-то, кроме адреса, следующего за инструкцией call. Но я думаю, что это иногда может произойти на процессоре ARM.

Поскольку инструкция ARM не всегда может содержать полные 32-битные непосредственные данные, обычно константы (числовые или строковые) «встраиваются» как данные в поток кода, поэтому значение или указатель на него могут загружаться с использованием относительного адреса pc (счетчик программы). Обычно эти константы находятся в месте, где не нужно совершать прыжок только из-за данных. Одним из наиболее распространенных мест для таких данных будет область между кодом для двух функций. Но еще одно место, где это условие выполняется после перехода, созданного для вызова функции, поскольку в любом случае необходимо выполнить переход, чтобы перейти к инструкциям, следующим за сайтом вызова (возврат из функции). Таким образом, это не повредит времени выполнения для размещения данных сразу после вызова и установки обратного адреса в качестве адреса, следующего за данными. Компилятор загружает регистр lr (который по соглашению используется для хранения адреса возврата) с адресом, следующим за данными, затем выдает безусловную ветвь функции. Вы можете видеть это не слишком часто, но похожие методы размещения данных в сегменте кода распространены в ARM.

0 голосов
/ 10 марта 2012

За исключением ситуаций с виртуальной памятью (когда RET может вызвать сбой страницы, что технически означает, что объект RET вызывает обработчик ошибок), я думаю, главное, что стоит обсудить, это то, что setjmp и longjmp могут полностью подорватьстек - так что вы можете на законных основаниях вызывать что-то, а затем сделать так, чтобы оно возвращало произвольное количество стековых кадров, даже не ударяя по RET.модифицированный стек - это будет зависеть от поставщика, как они хотели бы реализовать это.

...