Звоните _exit(0);
или exit_group(0)
в конце _start
. (Связывайте с gcc -static -nostartfiles
вместо -nostdlib
, чтобы вы могли вызывать функции-оболочки системных вызовов libc; они должны работать, даже если функции инициализации glibc не выполнялись , поэтому malloc или printf вылетали бы ).
Или сделать системный вызов exit_group(0)
вручную с помощью встроенного asm. В x86-64 Linux:
asm("mov $231, %eax; xor %edi,%edi; syscall");
См. Также Как получить значение аргументов, используя встроенную сборку в C без Glibc? для получения дополнительной информации о написании хакерского x86-64 _start
для запуска собственной функции C в качестве первой вещи в вашем процессе. (Но большая часть этого ответа касается взлома соглашения о вызовах для доступа к argc / argv, что является неприятным, и я не рекомендую его.) Ответ Маттео на этот вопрос имеет целый минимум _start
, записанный в asm, который вызывает нормальный C main
функция.
Код книги просто сломан по двум причинам . (Я не знаю, как это могло сработать на i386 или x86-64. Мне это кажется очень странным. Вы уверены, что это не должно было произойти сбой, но вы смотрите на то, что он делает до того, как это произойдет?)
_start
не является функцией в Linux; вы (или сгенерированный компилятором код) не можете ret
из него . Вам нужно сделать системный вызов _exit
. В стеке нет обратного адреса 1 .
Если функция имеет адрес возврата, точка входа ELF _start
имеет argc
, как указано в документах ABI. (x86-64 System V или i386 System V в зависимости от того, создаете ли вы 64-битный или gcc -m32
32-битный исполняемый файл.)
Вставка leave
(что соответствует mov %ebp, %esp
/ pop %ebp
или эквиваленту RBP / RSP) в сгенерированный компилятором код здесь не имеет смысла. Это что-то вроде дополнительного pop
, но нарушает EBP
/ RBP
компилятора, поэтому, если он выберет leave
вместо pop %rbp
для своего собственного пролога, сгенерированный компилятором код даст сбой. (RBP при входе в _start
равен 0 в статически связанном исполняемом файле. Или удерживает любой динамический компоновщик, оставленный в RBP, прежде чем перейти к _start
в исполняемом файле PIE.)
Но, в конечном итоге, GCC скомпилирует _start
как обычную функцию, таким образом, в конечном итоге выполняя инструкцию ret
. Нигде нет действительного / полезного обратного адреса, поэтому ret
вообще не может работать.
Если вы компилируете без оптимизации (по умолчанию), gcc по умолчанию будет -fno-omit-frame-pointer
, поэтому его пролог функции настроит EBP или RBP в качестве указателя кадра, что позволит самой leave
не вызывать ошибку. Если вы скомпилировали с оптимизацией (-O1
и выше включает -fomit-frame-pointer
), gcc не будет связываться с RBP, и будет равен нулю, когда вы запустите leave
, таким образом, непосредственно вызывая segfault. (Поскольку он выполняет RSP = RBP, а затем использует новый RSP в качестве указателя стека для pop %rbp
.)
В любом случае, если не произойдет сбой, указатель стека снова будет указывать на argc
, прежде чем сгенерированный компилятором pop %rbp
как часть нормального эпилога функции. Таким образом, сгенерированный компилятором ret
попытается вернуться к argv[0]
. Так как стек по умолчанию не исполняемый, это приведет к ошибке. (и он указывает на символы ASCII, которые, вероятно, не будут декодироваться как полезный машинный код x86-64.)
Вы могли бы выяснить это самостоятельно, пошагово выполняя задание с помощью GDB. (layout reg
и используйте stepi
aka si
).
В общем случае, если вы возитесь с указателем стека и другими регистрами за спиной компилятора, это обычно приводит к сбою. И если бы в стеке был адрес возврата выше, pop %rcx
был бы намного более разумным, чем leave
.
Сноска 1:
В адресном пространстве вашего процесса нет ни одного машинного кода, на который полезный обратный адрес мог бы сделать такой системный вызов, если только вы не внедрили некоторый машинный код в качестве аргумента или переменной среды. .
Вы связались с -nostdlib
, поэтому libc не связан. Если вы связали libc динамически, но по-прежнему записали свой собственный _start
(например, с gcc -nostartfiles
вместо полного -nostdlib
), ASLR означало бы, что функция libc _exit
была по некоторому адресу переменной времени выполнения.
Если вы статически связали libc (gcc -nostartfiles -static
), код для _exit()
не будет скопирован в ваш исполняемый файл, если вы на самом деле не ссылаетесь на него, чего нет в этом коде. Но вам все равно нужно как-то его вызвать; обратного адреса на него нет.