Выравнивание стека gcc x86-32 и вызов printf

Насколько мне известно, 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.

  1. Если помещаются два 4-байтовых значения, как достигается выравнивание?
  2. Почему отправляется 0x2014 вместо 0x14? Что такое 0x201d?
  3. Чего вообще добивается 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.

mov %esp, %ebp без сохранения / восстановления %ebp вызывающего абонента является плохим и может легко привести к сбою сегмента после основного возврата.
Peter Cordes 12.09.2018 06:04

Скомпилируйте свой код C с помощью gcc -O1 -fverbose-asm -S, чтобы получить код ассемблера. Прочтите соответствующую спецификацию x86 ABI

Basile Starynkevitch 12.09.2018 06:16

@BasileStarynkevitch Я пишу ассемблер для учебных целей. Я хочу сохранить как можно большую часть моей оригинальной сборки, от которой, боюсь, -O1 избавится.

qwr 12.09.2018 06:23

Затем удалите -O1. Обратите внимание, что ассемблер - это сгенерированный от gcc из некоторого исходного кода foo.c в foo.s после gcc -fverbose-asm -S foo.c (и вы можете добавить -O1). Я упомянул код C (не ассемблер), чтобы понять, какой код ассемблера генерирует gcc.

Basile Starynkevitch 12.09.2018 06:27

@BasileStarynkevitch Я все еще не совсем понимаю. Я использую as для создания объектного файла и gcc для создания исполняемого файла, вообще без источника C?

qwr 12.09.2018 06:28

Я предлагал скомпилировать простой код C (например, упомянутый в середине вашего вопроса) в код ассемблера и изучить ассемблер сгенерированный. Кстати, вам действительно не нужен gcc на чистой сборке (вы можете сделать прямой системные вызовы (2) без использования printf ... и использовать ld для получения исполняемого файла ELF). Читайте также Как сделать сборку Linux

Basile Starynkevitch 12.09.2018 06:29

Пожалуйста, укажите минимальный воспроизводимый пример в своем вопросе. Так что покажите весь код C, который у вас есть (если он у вас есть), весь код ассемблера и команды сборки (возможно, с as, ld, может быть, gcc). Предоставьте нам достаточно информации, чтобы воспроизвести ваше дело на компьютере наш. См. Также OSDEV. Кстати, а зачем вам printf?

Basile Starynkevitch 12.09.2018 06:33

Требование выравнивания стека - это ABI соглашение, привязанная к вашсоглашение о вызовах. Это нет, который требуется процессору x86-64 ISA (вы даже можете вызывать функции без, передавая аргументы в стеке, но это не соответствует обычному условности)

Basile Starynkevitch 12.09.2018 06:44

@BasileStarynkevitch Я добавил свои команды сборки и аргументы в пользу использования printf.

qwr 12.09.2018 07:22

Вы внимательно прочитали все ресурсы, на которые я ссылался? Они должны дать ответ на ваш вопрос. Я по-прежнему рекомендую скомпилировать крошечную программу на C foo.c, похожую на вашу программу на ассемблере (с gcc -S -fverbose-asm -O1 foo.c), и изучить сгенерированный код ассемблера foo.s. Он должен научить вас полезным вещам.

Basile Starynkevitch 12.09.2018 07:32

Что вам нужно глубоко понять, так это ваш ABI. Я дал достаточно ссылок и советов, чтобы понять это, но у вас есть несколько часов чтения

Basile Starynkevitch 12.09.2018 07:43

@qwr: версия i386 System V ABI, используемая в Linux, действительно требует / гарантирует 16-байтовое выравнивание стека перед call, как и x86-64 System V ABI. Можно ли улучшить формулировку в моем ответе, который вы связали, чтобы сделать это более ясным? printf может дать сбой, если он захочет, если он будет вызван с неверно выровненным стеком, как вы делаете в этом рукописном ассемблере. (Но, вероятно, не будет в системах, где libc скомпилирована без SSE).

Peter Cordes 12.09.2018 17:43

@PeterCordes Я полагаю, что мой вопрос № 1 на самом деле "почему не происходит сбой printf", если gcc не выполняет выравнивание

qwr 12.09.2018 18:28

Потому что он не делает никаких 16-байтовых копий в / из стека с помощью movaps или movdqa. Большинство функций на практике не зависят от гарантированного ABI выравнивания. scanf делает это в недавней x86-64 glibc, но раньше этого не делали. scanf Ошибка сегментации при вызове из функции, которая не изменяет RSP. Как я уже сказал, я не думаю, что Ubuntu вообще компилирует свою i386 glibc с SSE2, а 3 указателя занимают всего 12 байтов (а не 24), поэтому gcc, вероятно, все равно не будет использовать 16-байтовую копию.

Peter Cordes 12.09.2018 18:32

(Тем не менее, любая функция с double в стеке будет избегать разбиения строки кэша для нее, потому что они могут дать ей 8-байтовое выравнивание относительно известного 16-байтового выравнивания. Так что, безусловно, функции могут использовать это преимущество, но будут только сбой из-за несовпадения, если они используют SSE. Я думаю, вы можете представить себе какой-то расчет выравнивания указателя с И, который предполагает выравнивание для начала и приведет к неправильному поведению при несовпадении ...)

Peter Cordes 12.09.2018 18:36

@PeterCordes, хорошо, можешь опубликовать это в качестве ответа? Я никогда не слышал, когда функции делают и не заботятся о выравнивании, и я никогда не писал SSE. Конечно, следовать ABI - это хорошо.

qwr 12.09.2018 18:38
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
16
859
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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 12.09.2018 19:07

@qwr: да, вы создали исполняемый файл PIE, потому что это gcc по умолчанию в последних версиях Ubuntu.

Peter Cordes 12.09.2018 19:21

Спасибо за ссылку на PIE, это то, что я искал.

qwr 12.09.2018 19:45

Другие вопросы по теме