2016-08-02 4 views
0

현재 C 프로그램을 디스 어셈블하고 해당 기능을 이해하려고 어셈블리 읽기를 연습하고 있습니다.어셈블리 - 함수 호출에 매개 변수 전달

나는 단순한 안녕하세요 세계 프로그램입니다. 내가 주를 분해

#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 

나는 처음 두 줄의 이해 : 기본 포인터로 스택 포인터의 값을 발생 푸시 RBP에 의해 (스택에 저장됩니다 "성장"했기 때문에 8 씩 감소) 스택 포인터의 값이 기본 포인터에 저장 됨 (스택이 계속 성장할 수있는 동안 매개 변수와 로컬 변수가 각각 양수 및 음수 오프셋을 통해 쉽게 도달 할 수 있음) ").

세 번째 줄은 첫 번째 문제를 나타냅니다. 0x4005c4 ("Hello, World!"문자열의 주소)가 스택에서 이동하지 않고 edi 레지스터에서 이동 한 이유는 무엇입니까? printf 함수가 그 문자열의 주소를 매개 변수로 받아 들여야하지 않습니까? 내가 아는 한, 함수는 스택에서 매개 변수를 가져온다 (그러나 여기에서는 매개 변수가 그 레지스터에 저장되어있는 것처럼 보인다 : edi)

StackOverflow의 또 다른 게시물에서 "printf @ ptl"은 스텁과 같다. 실제 printf 함수를 호출하는 함수. EAX에

(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. 

두 MOV 작업 여기에 그들에게 역할을하지 않기 때문에 (mov 인 EAX는 0x0으로는)뿐만 아니라, 나에게 조금 귀찮게 : 더 혼란 그 기능을 분해하려고하지만 도착 (그러나 나는 지금 막 기술 한 것에 더 관심이있다). 미리 감사드립니다.

+2

[x86-64 함수 args 스택] (http://stackoverflow.com/search?q=x86-64+function+args+stack)에 대한 검색 관련 질문 내가 본 것 중 아무 것도 정확한 복제본처럼 보일 수는 없지만 다음 번에 혼란 스러울 때 관련 키워드 중 일부를 검색해보십시오. –

+0

제안 사항에 따라 주에서 분해하는 것은 때로는 까다 롭습니다. main에서 함수를 호출하고 그 함수를 처음부터 다시 시작하는 것이 거의 언제나 쉽습니다. –

답변

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 

를 사용하고 거기 당신은 귀하의 경우 사용 EDI를 이동합니다.

두 개의 포인터 이제 EDI 및 ESI가 사용되는

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 

EAX는, 그래서 아마도 EAX가 따라 매개 변수의 수와 함께 할 수있는 뭔가가 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 

및 7 매개 변수가 수정 스택에서 가져온 및 호출 전에 스택에 다시 넣어 충분히 확인을 시도 할 수 있습니다. 나머지 6 개는 레지스터에 있습니다.

+0

첫 번째 예제는'% rsp' 만 조정하므로'call' 전에 16B 정렬됩니다. args는 스택에 전달되지 않습니다. 그리고 네,'% al'은 SysV x86-64 ABI에서 xmm 레지스터에 전달 된 FP 인수의 수 (최대 8 개)를 유지합니다. 처음 6 정수 인수는 레지스터에 들어갑니다 (1 또는 2가 아님). –

+0

'$ 0x0'은 연결된 바이너리 대신'.o'를 디스 어셈블하거나'gcc -S' 출력을보고 있기 때문에 자리 표시 자입니다. 'objdump -dr'를 사용했다면, 그 줄에 심볼 재배치 정보가 나타납니다. –

+0

연결하지 않고서는 푸시가 주소에 대한 자리 표시 자인지 여부가 모호했습니다. 64 비트 주소의 경우 32 비트 즉석 오프셋이 명확하게 오프셋되었습니다. OP를 안심시키는 것만으로는 흥미롭지 않습니다. 결과는 컴파일, 디스 어셈블 및 검토를 통해 올바르게 처리됩니다. 문자열이 함수에서 처음 사용되기보다는 전달 되었다면 edi 수정은 발생하지 않았을 것입니다. OP가 확인하기위한 운동. –

5

gcc는 x86-64 System V ABI을 대상으로하며 Windows 이외의 모든 x86-64 시스템에서 사용됩니다 (various historical reasons). 그것의 호출 규칙은 스택에 떨어지기 전에 레지스터에있는 처음 몇 args를 전달합니다. (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 

포인터는 mov edi, imm32movabs rdi, imm64 대신 사용할 수있는 이유는, 32 비트에 맞지.

부동 소수점 인수는 SSE 레지스터 (xmm0-xmm7)에서 전달되며 var-args 함수에도 전달됩니다. al은 벡터 레지스터에 몇 개의 FP 인수가 있는지 나타냅니다. (C의 타입 프로모션 규칙은 float 가변 인수 함수가 항상 double으로 승격됨을 의미하므로 printf에는 float, doublelong double에 대한 형식 지정자가 없습니다.


[email protected] 실제 printf 함수를 호출하는 스텁 기능과 같다.

네, 맞습니다. 프로 시저 연결 표 항목은 심볼을 해석하고 PLB에서 코드를 수정하여 jmp으로 변환하여 libc의 printf 정의가 매핑 된 주소에 직접 연결하는 동적 링커 루틴에 jmp으로 시작합니다. printf__printf의 약한 별칭이기 때문에 gddb가 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의 구현은 VAR-인수는 로컬 어레이 순서 (형식 문자열 유지 제를 제외한) 모든 인수 통과 레지스터를 저장하여 간단한 처리 유지한다. 올바른 정수 또는 FP 인수를 추출하기 위해 스위치와 같은 코드가 필요하지 않고 포인터를 통과 할 수 있습니다.호출자가 스택에 푸시 한 나머지 args와 연속적이 아니기 때문에 처음 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 바이트의 스택을 낭비 할 필요는 없습니다. (내 대답과 다른 답변에 대한 의견을 참조하십시오 Why does Windows64 use a different calling convention from all other OSes on x86-64?)

관련 문제