Вызов printf предотвращает сегрегирование
Студент информатики здесь. Нас попросили поиграть с переключением контекста, и одно конкретное задание заставило нас реализовать довольно грубую систему try / throw. Вот код, который мы писали:
struct ctx_s {
int esp;
int ebp;
};
struct ctx_s * pctx;
typedef int (func_t)(int); /* a function that returns an int from an int */
int try(func_t *f, int arg)
{
/* saving context by storing values of %esp and %ebp */
asm ("movl %%esp, %0"
: "=r"((*pctx).esp)
:
);
asm ("movl %%ebp, %0"
: "=r"((*pctx).ebp)
:
);
/* calling the function sent to try(), returning whatever it returns */
return f(arg);
}
int throw(int r)
{
printf("MAGIC PRINT\n");
static int my_return = 0;
/* ^ to avoid "an element from initialisation is not a constant" */
my_return = r;
/* restituting context saved in try() */
asm ("movl %0, %%esp"
:
: "r"((*pctx).esp)
);
asm ("movl %0, %%ebp"
:
: "r"((*pctx).ebp)
);
/* this return will go back to main() since we've restored try()'s context
so the return address is whatever called try... */
/* my_return is static (=> stored in the heap) so it's not been corrupted,
unlike r which is now the second parameter received from try()'s context,
and who knows what that might be */
return my_return;
}
pctx - это глобальный указатель на простую структуру, содержащую два целых числа, f - это функция, которая вызывает throw (), отправляя некоторый код возврата #define'd в 42, а main () по существу выделяет pctx, действительно, result=try(f, 0) и печатает результат. Мы ожидаем, что результат будет 42.
Теперь вы, возможно, заметили ВОЛШЕБНЫЙ ПЕЧАТИ в throw (). Это здесь по причинам, не совсем понятным; в основном, большинство (не все) ученики были разбиты на сегменты внутри throw (); вызов printf () внутри этой функции заставил программу работать на вид правильно, и учителя считают, что любой системный вызов также сработал бы.
Так как я не получил их объяснения, я попытался сравнить ассемблерные коды, сгенерированные с помощью gcc -S для обеих версий (с и без printf()), но я не смог ничего сделать из этого. Установка точки останова на открывающей скобке throw () (строка 33) и разборка с помощью gdb дали мне следующее:
Без printf():
Breakpoint 1, throw (r=42) at main4.c:38
(gdb) disass
Dump of assembler code for function throw:
0x0804845a <throw+0>: push %ebp
0x0804845b <throw+1>: mov %esp,%ebp
0x0804845d <throw+3>: mov 0x8(%ebp),%eax
0x08048460 <throw+6>: mov %eax,0x8049720
0x08048465 <throw+11>: mov 0x8049724,%eax
0x0804846a <throw+16>: mov (%eax),%eax
0x0804846c <throw+18>: mov %eax,%esp
0x0804846e <throw+20>: mov 0x8049724,%eax
0x08048473 <throw+25>: mov 0x4(%eax),%eax
0x08048476 <throw+28>: mov %eax,%ebp
0x08048478 <throw+30>: mov 0x8049720,%eax
0x0804847d <throw+35>: pop %ebp
0x0804847e <throw+36>: ret
End of assembler dump.
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0xb7e846c0 in ?? ()
С printf():
Breakpoint 1, throw (r=42) at main4.c:34
(gdb) disassemble
Dump of assembler code for function throw:
0x0804845a <throw+0>: push %ebp
0x0804845b <throw+1>: mov %esp,%ebp
0x0804845d <throw+3>: sub $0x18,%esp
0x08048460 <throw+6>: movl $0x80485f0,(%esp)
0x08048467 <throw+13>: call 0x8048364 <puts@plt>
0x0804846c <throw+18>: mov 0x8(%ebp),%eax
0x0804846f <throw+21>: mov %eax,0x804973c
0x08048474 <throw+26>: mov 0x8049740,%eax
0x08048479 <throw+31>: mov (%eax),%eax
0x0804847b <throw+33>: mov %eax,%esp
0x0804847d <throw+35>: mov 0x8049740,%eax
0x08048482 <throw+40>: mov 0x4(%eax),%eax
0x08048485 <throw+43>: mov %eax,%ebp
0x08048487 <throw+45>: mov 0x804973c,%eax
0x0804848c <throw+50>: leave
0x0804848d <throw+51>: ret
End of assembler dump.
(gdb) c
Continuing.
MAGIC PRINT
result = 42
Program exited normally.
Я действительно не знаю, что с этим делать. Очевидно, что все происходит по-другому, но мне трудно понять, что происходит в обоих случаях... Итак, мой вопрос, по сути: как вызвать printf make throw, а не segfault?
2 ответа
Хорошо, это немного свободный анализ, так как я не вижу части try, но, судя по стандартным соглашениям о вызовах, ваш метод, содержащий try, сохранит %esp
в %ebp
, уменьшить %esp
освободить место для локальных переменных и запустить свой "пробный" код, который сохраняет %esp
а также %ebp
,
Обычно, когда функция завершается, она отменяет эти изменения, используя leave
до возвращения. Выйти восстановлю %ebp
в %esp
, поп %ebp
и сделать его возвращение. Это гарантирует, что %esp
восстанавливается до своей точки, прежде чем место для локальных переменных было зарезервировано.
Проблема в версии без printf
это то, что он не использует leave
который выскакивает %ebp
без предварительного восстановления его содержимого в %esp
, ret
инструкция выведет локальную переменную и вернется к ней. Не самый лучший результат.
Я подозреваю, что, поскольку ваша функция не имеет локальных переменных, компилятор не видит причин для восстановления %esp
от %ebp
, поскольку printf
резервирует место в стеке, компилятор знает в этой версии, что %esp
должны быть восстановлены до возвращения.
Если вы хотите проверить теорию, просто скомпилируйте ассемблер, замените;
0x0804847d <throw+35>: pop %ebp
с инструкцией выхода и собрать результат. Это должно работать так же хорошо.
С другой стороны, я подозреваю, что вы могли указать gcc в ваших инструкциях asm, что %esp
был засорен, и тем самым заставил его генерировать вместо этого отпуск.
РЕДАКТИРОВАТЬ: Видимо маркировка %esp
поскольку clobbered по сути является NOOP в gcc:-/
Вы "восстанавливаете" ESP
к значению, сохраненному в другой функции. Вероятно, не полезное значение здесь.
Разница с "магическим" кодом заключается в том, что он заставляет компилятор сохранять и восстанавливать кадр стека в throw
функция.
leave
инструкция в конце эквивалентна
mov %ebp, %esp
pop %ebp
который может просто вернуть указатель стека к тому, что было в записи функции.