2016-08-02 2 views
0

В настоящее время я занимаюсь чтением сборок, разборки программ на С и попытки понять, что они делают.Сборка - Передача параметров вызову функции

Я застрял с тривиальным: простая программа мира привет.

#include <stdio.h> 
#include <stdlib.h> 

int main() { 
    printf("Hello, world!"); 
    return(0); 
} 

Когда я разобрать главный:

(gdb) disassemble main 
Dump of assembler code for function main: 
    0x0000000000400526 <+0>: push rbp 
    0x0000000000400527 <+1>: mov rbp,rsp 
    0x000000000040052a <+4>: mov edi,0x4005c4 
    0x000000000040052f <+9>: mov eax,0x0 
    0x0000000000400534 <+14>: call 0x400400 <[email protected]> 
    0x0000000000400539 <+19>: mov eax,0x0 
    0x000000000040053e <+24>: pop rbp 
    0x000000000040053f <+25>: ret 

Я понимаю, первые две строки: базовый указатель сохраняется в стеке (выталкивания ПОР, что приводит к тому, значение указателя стека, чтобы быть уменьшено на 8, потому что оно «выросло»), а значение указателя стека сохраняется в базовом указателе (так что параметры и локальная переменная могут быть легко достигнуты с помощью положительных и отрицательных смещений соответственно, тогда как стек может продолжать «расти» «).

В третьей строке представлена ​​первая проблема: почему 0x4005c4 (адрес строки «Hello, World!») Перемещен в регистре edi вместо того, чтобы перемещать его в стек? Должна ли функция printf принимать адрес этой строки в качестве параметра? Для того, что я знаю, функции берут параметры из стека (но здесь похоже, что параметр помещается в этот регистр: edi)

В другом посте здесь, в StackOverflow, я читал, что «printf @ ptl» как заглушка которая вызывает реальную функцию printf. Я попытался разобрать эту функцию, но она становится еще более запутанной:

(gdb) disassemble printf 
Dump of assembler code for function __printf: 
    0x00007ffff7a637b0 <+0>: sub rsp,0xd8 
    0x00007ffff7a637b7 <+7>: test al,al 
    0x00007ffff7a637b9 <+9>: mov QWORD PTR [rsp+0x28],rsi 
    0x00007ffff7a637be <+14>: mov QWORD PTR [rsp+0x30],rdx 
    0x00007ffff7a637c3 <+19>: mov QWORD PTR [rsp+0x38],rcx 
    0x00007ffff7a637c8 <+24>: mov QWORD PTR [rsp+0x40],r8 
    0x00007ffff7a637cd <+29>: mov QWORD PTR [rsp+0x48],r9 
    0x00007ffff7a637d2 <+34>: je  0x7ffff7a6380b <__printf+91> 
    0x00007ffff7a637d4 <+36>: movaps XMMWORD PTR [rsp+0x50],xmm0 
    0x00007ffff7a637d9 <+41>: movaps XMMWORD PTR [rsp+0x60],xmm1 
    0x00007ffff7a637de <+46>: movaps XMMWORD PTR [rsp+0x70],xmm2 
    0x00007ffff7a637e3 <+51>: movaps XMMWORD PTR [rsp+0x80],xmm3 
    0x00007ffff7a637eb <+59>: movaps XMMWORD PTR [rsp+0x90],xmm4 
    0x00007ffff7a637f3 <+67>: movaps XMMWORD PTR [rsp+0xa0],xmm5 
    0x00007ffff7a637fb <+75>: movaps XMMWORD PTR [rsp+0xb0],xmm6 
    0x00007ffff7a63803 <+83>: movaps XMMWORD PTR [rsp+0xc0],xmm7 
    0x00007ffff7a6380b <+91>: lea rax,[rsp+0xe0] 
    0x00007ffff7a63813 <+99>: mov rsi,rdi 
    0x00007ffff7a63816 <+102>: lea rdx,[rsp+0x8] 
    0x00007ffff7a6381b <+107>: mov QWORD PTR [rsp+0x10],rax 
    0x00007ffff7a63820 <+112>: lea rax,[rsp+0x20] 
    0x00007ffff7a63825 <+117>: mov DWORD PTR [rsp+0x8],0x8 
    0x00007ffff7a6382d <+125>: mov DWORD PTR [rsp+0xc],0x30 
    0x00007ffff7a63835 <+133>: mov QWORD PTR [rsp+0x18],rax 
    0x00007ffff7a6383a <+138>: mov rax,QWORD PTR [rip+0x36d70f]  # 0x7ffff7dd0f50 
    0x00007ffff7a63841 <+145>: mov rdi,QWORD PTR [rax] 
    0x00007ffff7a63844 <+148>: call 0x7ffff7a5b130 <_IO_vfprintf_internal> 
    0x00007ffff7a63849 <+153>: add rsp,0xd8 
    0x00007ffff7a63850 <+160>: ret  
End of assembler dump. 

Две МОВ операции по EAX (MOV EAX, 0x0) беспокоит меня немного, как хорошо, так как я не получаю они роль здесь (но меня больше беспокоит то, что я только что описал). Спасибо заранее.

+2

Поиск [x86-64 function args stack] (http://stackoverflow.com/search?q=x86-64+function+args+stack) находит тонны связанных вопросов. Ни один из тех, на которые я смотрел, не похож на точный дубликат, но в следующий раз, когда вы озадачены, попробуйте найти некоторые из релевантных ключевых слов. –

+0

В качестве предложения, разборки с основного могут иногда быть сложными. Почти всегда легче начать с вызова функции от основной и разборки, чтобы начать с. –

ответ

2

нет ничего тривиального в printf, не первый выбор для того, что вы пытаетесь сделать, но оказалось не слишком сложным.

Что-то проще:

extern unsigned int more_fun (unsigned int); 
unsigned int fun (unsigned int x) 
{ 
    return(more_fun(x)+7); 
} 
0000000000000000 <fun>: 
    0: 48 83 ec 08    sub $0x8,%rsp 
    4: e8 00 00 00 00   callq 9 <fun+0x9> 
    9: 48 83 c4 08    add $0x8,%rsp 
    d: 83 c0 07    add $0x7,%eax 
    10: c3      retq 

и используется стек. eax используется для возврата.

теперь использовать указатель

extern unsigned int more_fun (unsigned int *); 
unsigned int fun (unsigned int x) 
{ 
    return(more_fun(&x)+7); 
} 
0000000000000000 <fun>: 
    0: 48 83 ec 18    sub $0x18,%rsp 
    4: 89 7c 24 0c    mov %edi,0xc(%rsp) 
    8: 48 8d 7c 24 0c   lea 0xc(%rsp),%rdi 
    d: e8 00 00 00 00   callq 12 <fun+0x12> 
    12: 48 83 c4 18    add $0x18,%rsp 
    16: 83 c0 07    add $0x7,%eax 
    19: c3      retq 

и там вы идете ЭДО используется как в вашем случае.

два указателя

extern unsigned int more_fun (unsigned int *, unsigned int *); 
unsigned int fun (unsigned int x, unsigned int y) 
{ 
    return(more_fun(&x,&y)+7); 
} 
0000000000000000 <fun>: 
    0: 48 83 ec 18    sub $0x18,%rsp 
    4: 89 7c 24 0c    mov %edi,0xc(%rsp) 
    8: 89 74 24 08    mov %esi,0x8(%rsp) 
    c: 48 8d 7c 24 0c   lea 0xc(%rsp),%rdi 
    11: 48 8d 74 24 08   lea 0x8(%rsp),%rsi 
    16: e8 00 00 00 00   callq 1b <fun+0x1b> 
    1b: 48 83 c4 18    add $0x18,%rsp 
    1f: 83 c0 07    add $0x7,%eax 
    22: c3      retq 

Теперь еди и еси используются. все смотрит, как он это соглашение о вызовах мне ...

строка

extern unsigned int more_fun (const char *); 
unsigned int fun (void ) 
{ 
    return(more_fun("Hello World")+7); 
} 
0000000000000000 <fun>: 
    0: 48 83 ec 08    sub $0x8,%rsp 
    4: bf 00 00 00 00   mov $0x0,%edi 
    9: e8 00 00 00 00   callq e <fun+0xe> 
    e: 48 83 c4 08    add $0x8,%rsp 
    12: 83 c0 07    add $0x7,%eax 
    15: c3      retq 

е не нацелен как в Printf, так что, возможно е что-то делать с числом параметров, которые следуют, попробуй добавив больше параметров на ваш printf и посмотрим, будет ли eax изменяться.

Если я добавлю -m32 в свою командную строку, тогда edi не будет использоваться.

00000000 <fun>: 
    0: 83 ec 18    sub $0x18,%esp 
    3: 68 00 00 00 00   push $0x0 
    8: e8 fc ff ff ff   call 9 <fun+0x9> 
    d: 83 c4 1c    add $0x1c,%esp 
    10: 83 c0 07    add $0x7,%eax 
    13: c3 

Я подозреваю, что толчок является заполнителем для линкера нажать адрес в строку, когда линкер латает бинарник, это просто объект. Поэтому я предполагаю, что когда у вас 64-разрядный указатель, первый или два входят в регистры, тогда стек используется после того, как у него закончились регистры.

Очевидно, что компилятор работает так, что это соответствует соглашению о составлении компиляторов.

extern unsigned int more_fun (unsigned int); 
unsigned int fun (unsigned int x) 
{ 
    return(more_fun(x+5)+7); 
} 
0000000000000000 <fun>: 
    0: 48 83 ec 08    sub $0x8,%rsp 
    4: 83 c7 05    add $0x5,%edi 
    7: e8 00 00 00 00   callq c <fun+0xc> 
    c: 48 83 c4 08    add $0x8,%rsp 
    10: 83 c0 07    add $0x7,%eax 
    13: c3      retq 

исправление, основанное на комментарии Петра. Да, похоже, что здесь используются регистры.

И поскольку он упомянул 6 параметров, позволяет попробовать 7.

extern unsigned int more_fun 
(
unsigned int, 
unsigned int, 
unsigned int, 
unsigned int, 
unsigned int, 
unsigned int, 
unsigned int 
); 
unsigned int fun (
unsigned int a, 
unsigned int b, 
unsigned int c, 
unsigned int d, 
unsigned int e, 
unsigned int f, 
unsigned int g 
) 
{ 
    return(more_fun(a+1,b+2,c+3,d+4,e+5,f+6,g+7)+17); 
} 
0000000000000000 <fun>: 
    0: 48 83 ec 10    sub $0x10,%rsp 
    4: 83 c1 04    add $0x4,%ecx 
    7: 83 c2 03    add $0x3,%edx 
    a: 8b 44 24 18    mov 0x18(%rsp),%eax 
    e: 83 c6 02    add $0x2,%esi 
    11: 83 c7 01    add $0x1,%edi 
    14: 41 83 c1 06    add $0x6,%r9d 
    18: 41 83 c0 05    add $0x5,%r8d 
    1c: 83 c0 07    add $0x7,%eax 
    1f: 50      push %rax 
    20: e8 00 00 00 00   callq 25 <fun+0x25> 
    25: 48 83 c4 18    add $0x18,%rsp 
    29: 83 c0 11    add $0x11,%eax 
    2c: c3      retq 

и достаточно уверен, что седьмой параметр был разобран из стека модифицированных и поместить обратно в стек перед вызовом. Остальные 6 в реестрах.

+0

Ваш первый пример настраивает '% rsp', поэтому он равен 16B перед' вызовом'. В стек нет аргументов. И да, '% al' содержит количество аргументов FP, переданных в xmm-регистрах (до 8) в ABI SysV x86-64. Первые 6 целых аргументов идут в регистры (не только 1 или 2). –

+0

'$ 0x0' является заполнителем, потому что вы разбирали' .o' вместо связанного двоичного файла или смотрели на вывод 'gcc -S'. Если вы использовали 'objdump -dr', вы увидите информацию о перемещении символа на этой строке. –

+0

Без привязки я был неясен в отношении того, был ли толчок заполнителем для адреса, 32-битный немедленный не имеет смысла, хотя для 64-битного адреса, очевидно, просто смещение. Неинтересно, что ссылки просто успокаивают ОП, они находятся на правильном пути с тем, что они делают (компиляция, разборка и исследование результатов). Если бы строка была передана, а не использовалась в первый раз в функции, модификация edi не была бы выполнена. Упражнение для подтверждения OP. –

5

gcc предназначен для использования x86-64 System V ABI, используемый всеми системами x86-64, отличными от Windows (для various historical reasons). Его вызывающая конвенция передает первые несколько аргументов в регистры, прежде чем вернуться в стек. (См. Также Wikipedia basic summary of this calling convention.)

И да, это отличается от жестких старых 32-битных условных соглашений, которые используют стек для всего. Это хорошая вещь. См. Также ссылку на теги для получения дополнительных ссылок на документы ABI и множество других материалов.

0x0000000000400526: push rbp 
    0x0000000000400527: mov rbp,rsp   # stack-frame boilerplate 
    0x000000000040052a: mov edi,0x4005c4 # first arg 
    0x000000000040052f: mov eax,0x0   # 0 FP args in vector registers 
    0x0000000000400534: call 0x400400 <[email protected]> 
    0x0000000000400539: mov eax,0x0   # return 0. If you'd compiled with optimization, this and the previous mov would be xor eax,eax 
    0x000000000040053e: pop rbp    # clean up stack frame 
    0x000000000040053f: ret 

указатели на статические данные вписываться в 32 бит, поэтому он может использовать mov edi, imm32 вместо movabs rdi, imm64.

Аргументы с плавающей точкой передаются в регистры SSE (xmm0-xmm7), даже для функций var-args. al указывает, сколько аргументов FP находятся в векторных регистрах. (Обратите внимание, что правила продвижения типа C означают, что float аргументы в отношении вариационных функций всегда повышаются до double, поэтому printf не имеет спецификаторов формата для float, только double и long double).


[email protected] подобна функции заглушки, которая вызывает реальную функцию PRINTF.

Да, это так. Вкладка «Ссылка на схему» начинается с jmp в динамическую компоновку компоновщика, которая разрешает символ и изменяет код в PLT, чтобы превратить его в jmp непосредственно на адрес, на котором отображается определение printf libc. printf - слабый псевдоним для __printf, поэтому gdb выбирает метку __printf для этого адреса после того, как вы попросили разборку printf.

Dump of assembler code for function __printf: 
    0x00007ffff7a637b0 <+0>: sub rsp,0xd8    # reserve space 
    0x00007ffff7a637b7 <+7>: test al,al     # check if there were any FP args 
    0x00007ffff7a637b9 <+9>: mov QWORD PTR [rsp+0x28],rsi # store the integer arg-passing registers to local scratch space 
    0x00007ffff7a637be <+14>: mov QWORD PTR [rsp+0x30],rdx 
    0x00007ffff7a637c3 <+19>: mov QWORD PTR [rsp+0x38],rcx 
    0x00007ffff7a637c8 <+24>: mov QWORD PTR [rsp+0x40],r8 
    0x00007ffff7a637cd <+29>: mov QWORD PTR [rsp+0x48],r9 
    0x00007ffff7a637d2 <+34>: je  0x7ffff7a6380b <__printf+91> # skip storing the FP arg-passing regs if there were no FP args 
    0x00007ffff7a637d4 <+36>: movaps XMMWORD PTR [rsp+0x50],xmm0 
    0x00007ffff7a637d9 <+41>: movaps XMMWORD PTR [rsp+0x60],xmm1 
    0x00007ffff7a637de <+46>: movaps XMMWORD PTR [rsp+0x70],xmm2 
    0x00007ffff7a637e3 <+51>: movaps XMMWORD PTR [rsp+0x80],xmm3 
    0x00007ffff7a637eb <+59>: movaps XMMWORD PTR [rsp+0x90],xmm4 
    0x00007ffff7a637f3 <+67>: movaps XMMWORD PTR [rsp+0xa0],xmm5 
    0x00007ffff7a637fb <+75>: movaps XMMWORD PTR [rsp+0xb0],xmm6 
    0x00007ffff7a63803 <+83>: movaps XMMWORD PTR [rsp+0xc0],xmm7 
     branch_target_from_test_je: 
    0x00007ffff7a6380b <+91>: lea rax,[rsp+0xe0]   # some more stuff 

Так реализация printf «s держит вар-арг обработки просто, сохраняя все ARG минуя регистры (кроме первого, держащего строку формата) для того, чтобы местные массивы. Он может прогуливать через них указатель вместо того, чтобы использовать код, подобный коммутатору, для извлечения правильного целого числа или аргумента FP.Он по-прежнему должен отслеживать первые 5 целых и первые 8 аргументов FP, потому что они не смежны с остальными аргументами, выдвинутыми вызывающим в стек.

Теневое пространство условного соглашения Windows 64-бит упрощает это на providing space for a function to dump its register args to the stack contiguous with the args already on the stack, но это не стоит тратить 32 байта стека на каждый вызов, IMO. (См. Мой ответ и комментарии по другим ответам на Why does Windows64 use a different calling convention from all other OSes on x86-64?)