2016-11-21 10 views
-1

Рассмотрит функцию C (с внешней связью), как следующему:Летучий/модифицированного адрес возврата

void f(void **p) 
{ 
    /* do something with *p */ 
} 

Теперь предположит, что f вызывается таким образом, таким образом, что p указует на адрес возврата f на стек, как в следующем коде (при условии, System V AMD64 ABI):

leaq -8(%rsp), %rdi 
callq f 

что может случиться так, что код f изменяет адрес возврата в стеке путем присвоения значения * р. Таким образом, компилятору придется обрабатывать обратный адрес в стеке как изменчивое значение. Как я могу сообщить компилятору gcc в моем случае, что адрес возврата нестабилен?

В противном случае компилятор может, по крайней мере, в принципе, генерировать следующий код для f:

pushq %rbp 
movq 8(%rsp), %r10 
pushq %r10 
## do something with (%rdi) 
popq %r10 
popq %rbp 
addq 8,%rsp 
jmpq *%r10 

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

P.S .: Как было предложено Питером Кордесом, я должен лучше объяснить цель моего вопроса: речь идет о сборке мусора динамически сгенерированного машинного кода с помощью подвижного сборщика мусора. Функция f означает сборщик мусора. Вызов f может быть функцией, код которой перемещается во время работы f, поэтому я придумал идею о том, что f знает адрес возврата, так что f может его соответствующим образом изменить, независимо от того, указывает ли область памяти адрес возврата был перемещен или нет.

+3

Все это неопределенное поведение –

+0

В C99, конечно. Но это не означает, что он не будет иметь определенного поведения в реальной реализации, такой как gcc, используя правильные атрибуты функции/прагмы. И об этом мой вопрос. – Marc

+1

Вернитесь, когда у вас есть реальная проблема в реальном коде, который действительно компилируется. Теоретическое обсуждение вопросов, связанных с реализацией, утомительно. –

ответ

2

Использование SysV ABI (Linux, FreeBSD, Solaris, Mac OS X/macOS) на AMD64/x86-64, вам нужна только тривиальная функция сборки, обернутая вокруг фактической функции сборщика мусора.

f.s определяет void f(void *) и называет реальный GC, real_f(void *, void **), с добавленным вторым параметром, указывающим на обратный адрес.

.file  "f.s" 
    .text 

    .p2align 4,,15 
    .globl  f 
    .type  f, @function 

f: 
    movq  %rsp, %rsi 
    call  real_f 
    ret 

    .size  f, .-f 

Если real_f() уже есть два других параметра, используйте %rdx (для третьего) вместо %rsi. Если три-пять, используйте %rcx, %r8, или %r9, соответственно. SysV ABI на AMD64/x86-64 поддерживает только до шести параметров без плавающей запятой в регистрах.

Давайте протестируем выше с небольшим example.c:

#include <stdlib.h> 
#include <stdio.h> 

extern void f(void *); 

void real_f(void *arg, void **retval) 
{ 
    printf("real_f(): Returning to %p instead of %p.\n", arg, *retval); 
    *retval = arg; 
} 

int main(void) 
{ 
    printf("Function and label addresses:\n"); 
    printf("%p f()\n", f); 
    printf("%p real_f()\n", real_f); 
    printf("%p one_call:\n", &&one_call); 
    printf("%p one_fail:\n", &&one_fail); 
    printf("%p one_skip:\n", &&one_skip); 
    printf("\n"); 

    printf("f(one_skip):\n"); 
    fflush(stdout); 

one_call: 
    f(&&one_skip); 

one_fail: 
    printf("At one_fail.\n"); 
    fflush(stdout); 

one_skip: 
    printf("At one_skip.\n"); 
    fflush(stdout); 

    return EXIT_SUCCESS; 
} 

Обратите внимание, что выше полагается на обоих НКУ поведения (&&, обеспечивающий адрес метки), а также поведение GCC на AMD64/x86-64 архитектуры (объектные и функциональные указатели являются взаимозаменяемыми), а также компилятор C, не выполняющий ни одну из множества оптимизаций, которые им разрешено делать с кодом в main().

(Не имеет значения, оптимизирован ли real_f(), просто я был слишком ленив, чтобы выработать лучший пример в main().Например, тот, который создает небольшую функцию в исполняемом сегменте данных, который вызывает f(), с real_f(), перемещающим этот сегмент данных и соответствующим образом регулирующим обратный адрес. Это будет соответствовать сценарию OP, и это всего лишь единственный практический пример использования такого рода манипуляций, о котором я могу думать. Вместо этого, я просто взломал грубый пример, который может или не может работать для других.)

Кроме того, мы могли бы пожелать, чтобы объявить f() как имеющие два параметра (они будут переданы в %rdi и %rsi) тоже со вторым существом неуместно, чтобы убедиться, что компилятор не ожидает, что %rsi останется без изменений. (Если я правильно помню, SysV ABI позволяет нам избить его, но я мог бы вспомнить неправильно.)

На этой конкретной машине, компиляции выше с

gcc -Wall -O0 f.s example.c -o example 

Забегая

./example 

производит

Function and label addresses: 
0x400650 f() 
0x400659 real_f() 
0x400729 one_call: 
0x400733 one_fail: 
0x40074c one_skip: 

f(one_skip): 
real_f(): Returning to 0x40074c instead of 0x400733. 
At one_skip. 

Обратите внимание, что если вы говорите GCC оптимизировать код (скажем, -O2), он сделает предположения о коде в main(), это вполне допустимо сделать по стандарту C, но это может привести ко всем трем ярлыкам, имеющим точный адрес. Это происходит на моей конкретной машине и GCC-5.4.0 и, конечно, вызывает бесконечный цикл. Он не отражается на реализации f() или real_f() вообще, только мой пример в main() довольно беден. Мне лень.