Насколько мне известно, x86-64 требует, чтобы стек был выровнен по 16 байт перед вызовом, а gcc с -m32
не требует этого для основного.
У меня есть следующий тестовый код:
.data
intfmt: .string "int: %d\n"
testint: .int 20
.text
.globl main
main:
mov %esp, %ebp
push testint
push $intfmt
call printf
mov %ebp, %esp
ret
Сборка с as --32 test.S -o test.o && gcc -m32 test.o -o test
. Я знаю, что запись syscall существует, но, насколько мне известно, она не может печатать целые числа и плавать, как printf.
После входа в main в стеке находится 4-байтовый адрес возврата. Затем, наивно интерпретируя этот код, каждый из двух push-вызовов помещает в стек 4 байта, поэтому для выравнивания требуется еще одно 4-байтовое значение.
Вот objdump двоичного файла, сгенерированного газом и gcc:
0000053d <main>:
53d: 89 e5 mov %esp,%ebp
53f: ff 35 1d 20 00 00 pushl 0x201d
545: 68 14 20 00 00 push $0x2014
54a: e8 fc ff ff ff call 54b <main+0xe>
54f: 89 ec mov %ebp,%esp
551: c3 ret
552: 66 90 xchg %ax,%ax
554: 66 90 xchg %ax,%ax
556: 66 90 xchg %ax,%ax
558: 66 90 xchg %ax,%ax
55a: 66 90 xchg %ax,%ax
55c: 66 90 xchg %ax,%ax
55e: 66 90 xchg %ax,%ax
Я очень смущен генерируемыми инструкциями push.
call 54b
? Вывод hd
соответствует objdump
. Почему в GDB все по-другому? Это динамический компоновщик?B+>│0x5655553d <main> mov %esp,%ebp │
│0x5655553f <main+2> pushl 0x5655701d │
│0x56555545 <main+8> push $0x56557014 │
│0x5655554a <main+13> call 0xf7e222d0 <printf> │
│0x5655554f <main+18> mov %ebp,%esp │
│0x56555551 <main+20> ret
Приветствуются ресурсы о том, что происходит при фактическом выполнении двоичного файла, поскольку я не знаю, что на самом деле происходит, и учебные пособия, которые я прочитал, не охватывают это. Читаю через Как запускаются программы: двоичные файлы ELF.
Скомпилируйте свой код C с помощью gcc -O1 -fverbose-asm -S
, чтобы получить код ассемблера. Прочтите соответствующую спецификацию x86 ABI
@BasileStarynkevitch Я пишу ассемблер для учебных целей. Я хочу сохранить как можно большую часть моей оригинальной сборки, от которой, боюсь, -O1
избавится.
Затем удалите -O1
. Обратите внимание, что ассемблер - это сгенерированный от gcc
из некоторого исходного кода foo.c
в foo.s
после gcc -fverbose-asm -S foo.c
(и вы можете добавить -O1
). Я упомянул код C (не ассемблер), чтобы понять, какой код ассемблера генерирует gcc
.
@BasileStarynkevitch Я все еще не совсем понимаю. Я использую as
для создания объектного файла и gcc
для создания исполняемого файла, вообще без источника C?
Я предлагал скомпилировать простой код C (например, упомянутый в середине вашего вопроса) в код ассемблера и изучить ассемблер сгенерированный. Кстати, вам действительно не нужен gcc
на чистой сборке (вы можете сделать прямой системные вызовы (2) без использования printf
... и использовать ld
для получения исполняемого файла ELF). Читайте также Как сделать сборку Linux
Пожалуйста, укажите минимальный воспроизводимый пример в своем вопросе. Так что покажите весь код C, который у вас есть (если он у вас есть), весь код ассемблера и команды сборки (возможно, с as
, ld
, может быть, gcc
). Предоставьте нам достаточно информации, чтобы воспроизвести ваше дело на компьютере наш. См. Также OSDEV. Кстати, а зачем вам printf
?
Требование выравнивания стека - это ABI соглашение, привязанная к вашсоглашение о вызовах. Это нет, который требуется процессору x86-64 ISA (вы даже можете вызывать функции без, передавая аргументы в стеке, но это не соответствует обычному условности)
@BasileStarynkevitch Я добавил свои команды сборки и аргументы в пользу использования printf.
Вы внимательно прочитали все ресурсы, на которые я ссылался? Они должны дать ответ на ваш вопрос. Я по-прежнему рекомендую скомпилировать крошечную программу на C foo.c
, похожую на вашу программу на ассемблере (с gcc -S -fverbose-asm -O1 foo.c
), и изучить сгенерированный код ассемблера foo.s
. Он должен научить вас полезным вещам.
Что вам нужно глубоко понять, так это ваш ABI. Я дал достаточно ссылок и советов, чтобы понять это, но у вас есть несколько часов чтения
@qwr: версия i386 System V ABI, используемая в Linux, действительно требует / гарантирует 16-байтовое выравнивание стека перед call
, как и x86-64 System V ABI. Можно ли улучшить формулировку в моем ответе, который вы связали, чтобы сделать это более ясным? printf
может дать сбой, если он захочет, если он будет вызван с неверно выровненным стеком, как вы делаете в этом рукописном ассемблере. (Но, вероятно, не будет в системах, где libc скомпилирована без SSE).
@PeterCordes Я полагаю, что мой вопрос № 1 на самом деле "почему не происходит сбой printf
", если gcc не выполняет выравнивание
Потому что он не делает никаких 16-байтовых копий в / из стека с помощью movaps
или movdqa
. Большинство функций на практике не зависят от гарантированного ABI выравнивания. scanf
делает это в недавней x86-64 glibc, но раньше этого не делали. scanf Ошибка сегментации при вызове из функции, которая не изменяет RSP. Как я уже сказал, я не думаю, что Ubuntu вообще компилирует свою i386 glibc с SSE2, а 3 указателя занимают всего 12 байтов (а не 24), поэтому gcc, вероятно, все равно не будет использовать 16-байтовую копию.
(Тем не менее, любая функция с double
в стеке будет избегать разбиения строки кэша для нее, потому что они могут дать ей 8-байтовое выравнивание относительно известного 16-байтового выравнивания. Так что, безусловно, функции могут использовать это преимущество, но будут только сбой из-за несовпадения, если они используют SSE. Я думаю, вы можете представить себе какой-то расчет выравнивания указателя с И, который предполагает выравнивание для начала и приведет к неправильному поведению при несовпадении ...)
@PeterCordes, хорошо, можешь опубликовать это в качестве ответа? Я никогда не слышал, когда функции делают и не заботятся о выравнивании, и я никогда не писал SSE. Конечно, следовать ABI - это хорошо.
I386 System V ABI делает гарантирует / требует 16-байтового выравнивания стека перед call
, как я сказал в верхней части моего ответа, который вы связали. (Если вы не вызываете частную вспомогательную функцию, и в этом случае вы можете создать свои собственные правила для выравнивания, передачи аргументов и того, какие регистры затираются для этой функции.)
Функции: разрешается для сбоя или неправильного поведения, если вы нарушаете это требование ABI, но это не обязательно. например scanf
в x86-64 Ubuntu glibc (как скомпилирован недавним gcc) только недавно начал это делать: scanf Ошибка сегментации при вызове из функции, которая не изменяет RSP
Функции могут зависеть от выравнивания стека для обеспечения производительности (для выравнивания double
или массива double
, чтобы избежать разделения строк кэша при доступе к ним).
Обычно единственный случай, когда функция зависит от выравнивания стека для правильность, - это когда функция скомпилирована для использования SSE / SSE2, поэтому она может использовать загрузку / сохранение с 16-байтовым выравниванием для копирования структуры или массива (movaps
или movdqa
) или фактически автоматическая векторизация цикла по локальному массиву.
Я думаю, что Ubuntu не компилирует свои 32-битные библиотеки с SSE (за исключением таких функций, как memcpy
, которые используют диспетчеризацию во время выполнения), поэтому они все еще могут работать на старых процессорах, таких как Pentium II. Мультиархитектурные библиотеки в системе x86-64 должны использовать SSE2, но с 4-байтовыми указателями менее вероятно, что 32-битные функции будут иметь 16-байтовые структуры для копирования.
В любом случае, какой бы ни была причина, очевидно, что printf
в вашей 32-битной сборке glibc на самом деле не зависит от 16-байтового выравнивания стека для правильности, поэтому он не дает сбоев, даже если вы неправильно выравниваете стек.
Why is 0x2014 pushed instead of 0x14? What is 0x201d?
0x14
(десятичное число 20) - это значение в памяти в этом месте. Он будет загружен во время выполнения, потому что вы использовали push r/m32
, а не push $20
(или константу времени сборки, такую как .equ testint, 20
или testint = 20
).
Вы использовали gcc -m32
для создания PIE (независимый от позиции исполняемый файл), который перемещается во время выполнения., потому что это значение по умолчанию в gcc Ubuntu.
0x2014
- это смещение относительно начала файла. Если вы выполните дизассемблирование во время выполнения после запуска программы, вы увидите настоящий адрес.
То же и для call 54b
. Предположительно, это вызов PLT (который находится рядом с началом сегмента файла / текста, отсюда и младший адрес).
Если вы разобрали с помощью objdump -drwC
, вы увидите информацию о перемещении символов. (Мне тоже нравится -Mintel
, но будьте осторожны, он похож на MASM, а не на NASM).
Вы можете связать с gcc -m32 -no-pie
, чтобы сделать классические исполняемые файлы position-зависимый. Я определенно рекомендую это, особенно для 32-битного кода, и особенно если вы компилируете C, использовать gcc -m32 -no-pie -fno-pie
для получения генерации кода, отличного от PIE, а также для связывания с исполняемым файлом без PIE. (см. 32-битные абсолютные адреса больше не разрешены в x86-64 Linux? для получения дополнительной информации о PIE.)
И заменяются ли значения 0x2014 и 0x201d во время динамического связывания? Я использую Ubuntu 18.04 по умолчанию и все, что было с printf
.
@qwr: да, вы создали исполняемый файл PIE, потому что это gcc по умолчанию в последних версиях Ubuntu.
Спасибо за ссылку на PIE, это то, что я искал.
mov %esp, %ebp
без сохранения / восстановления%ebp
вызывающего абонента является плохим и может легко привести к сбою сегмента после основного возврата.