Почему в стеке есть дыры при передаче параметров? - PullRequest
0 голосов
/ 28 сентября 2018

Я не совсем знаком с ассемблерным кодом.Извините, если этот вопрос наивный.

У меня есть простая программа на C:

int f1(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9)
{
  int c = 3;
  int d = 4;
  return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + c + d;
}

int main(int argc, char** argv)
{
  f1(1, 2, 3, 4, 5, 6, 7, 8, 9);
}

Я скомпилировал ее в elf64-x86-64 и получил разборку нижекод:

f1 ():

0000000000000000 <f1>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)      ; 1
   7:   89 75 e8                mov    %esi,-0x18(%rbp)       ; 2
   a:   89 55 e4                mov    %edx,-0x1c(%rbp)      ; 3
   d:   89 4d e0                mov    %ecx,-0x20(%rbp)      ; 4
  10:   44 89 45 dc             mov    %r8d,-0x24(%rbp)  ; 5
  14:   44 89 4d d8             mov    %r9d,-0x28(%rbp)  ; 6
  18:   c7 45 f8 03 00 00 00    movl   $0x3,-0x8(%rbp) ; c = 3
  1f:   c7 45 fc 04 00 00 00    movl   $0x4,-0x4(%rbp) ; d = 4
  26:   8b 45 e8                mov    -0x18(%rbp),%eax     ;2
  29:   8b 55 ec                mov    -0x14(%rbp),%edx    ; 1
  2c:   01 c2                   add    %eax,%edx                
  2e:   8b 45 e4                mov    -0x1c(%rbp),%eax     ;3
  31:   01 c2                   add    %eax,%edx
  33:   8b 45 e0                mov    -0x20(%rbp),%eax     ;4
  36:   01 c2                   add    %eax,%edx
  38:   8b 45 dc                mov    -0x24(%rbp),%eax     ;5
  3b:   01 c2                   add    %eax,%edx
  3d:   8b 45 d8                mov    -0x28(%rbp),%eax    ; 6
  40:   01 c2                   add    %eax,%edx
  42:   8b 45 10                mov    0x10(%rbp),%eax     ;7
  45:   01 c2                   add    %eax,%edx
  47:   8b 45 18                mov    0x18(%rbp),%eax    ; 8
  4a:   01 c2                   add    %eax,%edx
  4c:   8b 45 20                mov    0x20(%rbp),%eax    ; 9
  4f:   01 c2                   add    %eax,%edx
  51:   8b 45 f8                mov    -0x8(%rbp),%eax    ; c =3
  54:   01 c2                   add    %eax,%edx
  56:   8b 45 fc                mov    -0x4(%rbp),%eax    ; d =4
  59:   01 d0                   add    %edx,%eax
  5b:   5d                      pop    %rbp
  5c:   c3                      retq   

main ():

000000000000005d <main>:
  5d:   55                      push   %rbp
  5e:   48 89 e5                mov    %rsp,%rbp
  61:   48 83 ec 30             sub    $0x30,%rsp
  65:   89 7d fc                mov    %edi,-0x4(%rbp)
  68:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  6c:   c7 44 24 10 09 00 00    movl   $0x9,0x10(%rsp)
  73:   00 
  74:   c7 44 24 08 08 00 00    movl   $0x8,0x8(%rsp)
  7b:   00 
  7c:   c7 04 24 07 00 00 00    movl   $0x7,(%rsp)
  83:   41 b9 06 00 00 00       mov    $0x6,%r9d
  89:   41 b8 05 00 00 00       mov    $0x5,%r8d
  8f:   b9 04 00 00 00          mov    $0x4,%ecx
  94:   ba 03 00 00 00          mov    $0x3,%edx
  99:   be 02 00 00 00          mov    $0x2,%esi
  9e:   bf 01 00 00 00          mov    $0x1,%edi
  a3:   b8 00 00 00 00          mov    $0x0,%eax
  a8:   e8 00 00 00 00          callq  ad <main+0x50>
  ad:   c9                      leaveq 
  ae:   c3                      retq   

Кажется, в стеке есть дыр , когдаПередача параметров от main() до f1():

enter image description here

Мои вопросы:

  • Зачем нужноэти дырки?

  • А зачем нам меньше 2 линий сборки?Если они предназначены для восстановления контекста, я не вижу никаких инструкций для этого.И регистр %rsi даже не используется в других местах.Зачем еще сохранять %rsi в стеке?

65: 89 7d fc mov %edi,-0x4(%rbp) 68: 48 89 75 f0 mov %rsi,-0x10(%rbp)

  • И только что задал еще один вопрос, поскольку аргументы 1 ~ 6 ужебыли переданы через регистры , зачем перемещать их обратно в память в начале f1()?

1 Ответ

0 голосов
/ 28 сентября 2018

Передача аргументов в x86-64 System V ABI использует 8-байтовые «слоты» в стеке для аргументов, которые не помещаются в регистры.Все, что не кратно 8 байтам, будет иметь дыры (заполнение) перед следующим аргументом стека.

Это довольно стандартно для соглашений о вызовах для операционных систем / архитектур.Передача short в 32-битном соглашении о вызовах будет использовать 4-байтовый слот стека (или займет весь 4-байтовый регистр, независимо от того, будет ли он расширяться до полной ширины регистра).


Ваши последние два вопроса действительно задают одно и то же:

Вы компилируете без оптимизации, поэтому для согласованной отладки каждой переменной, включая аргументы функции, нужен адрес памяти, где отладчик мог бы изменить значение при остановкев точке останова.Сюда входят main argc и argv, а также аргументы регистра f1.

Если вы определили main как int main(void) (что является одной из двух действительных сигнатур)для main в реализациях на хосте C, другое - int main(int argc, char**argv)), не будет входящих аргументов для разлива main.


Если вы скомпилировали с включенной оптимизацией, там 'не будь ничего из этого дерьма .См. Как удалить "шум" из выходных данных сборки GCC / clang? для предложений о том, как заставить компиляторы создавать asm, на которые приятно смотреть.например, из проводника компилятора Godbolt , скомпилированного с gcc -O3 -fPIC 1 , вы получите:

f1:
    addl    %esi, %edi      # a2, tmp106    # tmp106 = a1 + a2
    movl    8(%rsp), %eax   # a7, tmp110
    addl    %edx, %edi      # a3, tmp107
    addl    %ecx, %edi      # a4, tmp108
    addl    %r8d, %edi      # a5, tmp109
    addl    %r9d, %edi      # a6, tmp110
    addl    %edi, %eax      # tmp110, tmp110
    addl    16(%rsp), %eax  # a8, tmp112
    addl    24(%rsp), %eax  # a9, tmp113
    addl    $7, %eax        #, tmp105       # c+d = constant 7
    ret     

(я использовал синтаксис AT & T вместо Intel, потому что вы использовали этов вашем вопросе)

IDK, почему именно gcc резервирует несколько больше стекового пространства, чем на самом деле нужно;это иногда происходит даже при включенной оптимизации.например, gcc main выглядит следующим образом:

# gcc -O3
main:
    subq    $16, %rsp    # useless; the space isn't used and it doesn't change stack alignment.
    movl    $6, %r9d
    movl    $5, %r8d
    movl    $4, %ecx
    pushq   $9
    movl    $3, %edx
    movl    $2, %esi
    movl    $1, %edi
    pushq   $8
    pushq   $7
    call    f1@PLT
    xorl    %eax, %eax    # implicit return 0
    addq    $40, %rsp
    ret

Вся лишняя хрень, которая происходит в вашей версии функции, является следствием антиоптимизаций, необходимых для согласованной отладки, которую вы получаетесо значением по умолчанию -O0. (Последовательная отладка означает, что вы можете set переменную, когда она остановлена ​​на точке останова, и даже jump для другой строки источника внутри той же функции, и программа все равно будет работать и работать каквы можете ожидать от абстрактной машины C. Таким образом, компилятор не может хранить ничего в регистрах между операторами или оптимизировать на основе чего-либо, кроме литеральных констант внутри оператора.)

-O0 также означает быструю компиляциюи не пытайтесь эффективно распределить пространство стека.


Сноска 1: -fPIC не позволяет gcc оптимизировать вызов в main.

Без этого, даже если__attribute__((noinline)), он может видеть, что у функции нет побочных эффектов, поэтому он может просто пропустить вызов вместо того, чтобы включить его и оптимизировать.

Но-fPIC означает создание кода для разделяемой библиотеки, что (при нацеливании на Linux) означает, что возможно взаимное расположение символов, поэтому компилятор не может предположить, что call f1@plt фактически вызовет это определение f1, итаким образом, не может оптимизировать, основываясь на отсутствии побочных эффектов.

clang, очевидно, предполагает, что он все еще может оптимизировать таким образом даже с -fPIC, поэтому я предполагаю, что clang предполагает, что конфликтующие определения одной и той же функции недопустимыили что-то?Это может нарушить переопределения библиотечных функций LD_PRELOAD для вызовов из библиотеки.

...