2010-09-06 39 views
14

Tôi đang cố gắng hiểu mã mức lắp ráp cho một chương trình C đơn giản bằng cách kiểm tra nó bằng bộ tách rời của gdb.Hiểu mã lắp ráp được tạo bởi một chương trình C đơn giản

Tiếp theo là mã C:

#include <stdio.h> 

void function(int a, int b, int c) { 
    char buffer1[5]; 
    char buffer2[10]; 
} 

void main() { 
    function(1,2,3); 
} 

Sau đây là đoạn code tháo gỡ cho cả mainfunction

gdb) disass main 
Dump of assembler code for function main: 
0x08048428 <main+0>: push %ebp 
0x08048429 <main+1>: mov %esp,%ebp 
0x0804842b <main+3>: and $0xfffffff0,%esp 
0x0804842e <main+6>: sub $0x10,%esp 
0x08048431 <main+9>: movl $0x3,0x8(%esp) 
0x08048439 <main+17>: movl $0x2,0x4(%esp) 
0x08048441 <main+25>: movl $0x1,(%esp) 
0x08048448 <main+32>: call 0x8048404 <function> 
0x0804844d <main+37>: leave 
0x0804844e <main+38>: ret 
End of assembler dump. 

(gdb) disass function 
Dump of assembler code for function function: 
0x08048404 <function+0>: push %ebp 
0x08048405 <function+1>: mov %esp,%ebp 
0x08048407 <function+3>: sub $0x28,%esp 
0x0804840a <function+6>: mov %gs:0x14,%eax 
0x08048410 <function+12>: mov %eax,-0xc(%ebp) 
0x08048413 <function+15>: xor %eax,%eax 
0x08048415 <function+17>: mov -0xc(%ebp),%eax 
0x08048418 <function+20>: xor %gs:0x14,%eax 
0x0804841f <function+27>: je  0x8048426 <function+34> 
0x08048421 <function+29>: call 0x8048340 <[email protected]> 
0x08048426 <function+34>: leave 
0x08048427 <function+35>: ret  
End of assembler dump. 

Tôi đang tìm kiếm câu trả lời cho những điều sau đây:

  1. như thế nào địa chỉ đang hoạt động, ý tôi là (main + 0), (main + 1), (main + 3)
  2. Trong chính, tại sao là $ 0xfffffff0,% esp được sử dụng
  3. Trong hàm, tại sao% gs: 0x14,% eax,% eax, -0xc (% ebp) đang được sử dụng.
  4. Nếu ai đó có thể giải thích, từng bước xảy ra, điều đó sẽ được đánh giá cao.
+4

Bạn quên thẻ bài tập về nhà. –

+0

Đối với mỗi điểm trong số này, bạn có thể giải thích câu trả lời của bạn là gì, và sau đó nếu bạn tắt nó có thể được làm rõ. –

+3

không có điều này là không có bài tập về nhà .. – Adi

Trả lời

40

Lý do cho các địa chỉ "lạ" như main+0, main+1, main+3, main+6 và như vậy, là bởi vì mỗi lệnh chiếm một số biến của byte. Ví dụ:

main+0: push %ebp 

là hướng dẫn một byte để hướng dẫn tiếp theo là main+1. Mặt khác,

main+3: and $0xfffffff0,%esp 

là hướng dẫn ba byte để hướng dẫn tiếp theo sau đó là main+6.

Và, vì bạn hỏi ý kiến ​​tại sao movl dường như nhận một số byte thay đổi, giải thích cho điều đó như sau.

dài Hướng dẫn không chỉ phụ thuộc vào opcode (như movl) mà còn các phương thức giải quyết cho toán hạng cũng như (những điều opcode đang hoạt động trên). Tôi đã không kiểm tra đặc biệt cho mã của bạn nhưng tôi nghi ngờ sự hướng dẫn

movl $0x1,(%esp) 

có lẽ là ngắn hơn vì không có bù đắp tham gia - nó chỉ sử dụng esp như địa chỉ. Trong khi đó, một cái gì đó như:

movl $0x2,0x4(%esp) 

đòi hỏi tất cả những gì movl $0x1,(%esp) làm, cộng một byte thêm cho bù đắp 0x4.

Trong thực tế, đây là một phiên debug hiển thị những gì tôi muốn nói:

Microsoft Windows XP [Version 5.1.2600] 
(C) Copyright 1985-2001 Microsoft Corp. 

c:\pax> debug 
-a 
0B52:0100 mov word ptr [di],7 
0B52:0104 mov word ptr [di+2],8 
0B52:0109 mov word ptr [di+0],7 
0B52:010E 
-u100,10d 
0B52:0100 C7050700  MOV  WORD PTR [DI],0007 
0B52:0104 C745020800 MOV  WORD PTR [DI+02],0008 
0B52:0109 C745000700 MOV  WORD PTR [DI+00],0007 
-q 
c:\pax> _ 

Bạn có thể thấy rằng chỉ lệnh thứ hai với một bù đắp thực sự là khác nhau để là người đầu tiên mà không có nó. Đó là một byte dài hơn (5 byte thay vì 4 byte, để giữ khoảng trống) và thực sự có mã hóa khác nhau c745 thay vì c705.

Bạn cũng có thể thấy rằng bạn có thể mã hóa lệnh đầu tiên và thứ ba theo hai cách khác nhau nhưng về cơ bản chúng cũng thực hiện tương tự.


Các hướng dẫn and $0xfffffff0,%esp là một cách để buộc esp phải được trên một ranh giới cụ thể. Điều này được sử dụng để đảm bảo sự liên kết thích hợp của các biến. Nhiều bộ nhớ truy cập trên các bộ vi xử lý hiện đại sẽ hiệu quả hơn nếu chúng tuân thủ các quy tắc căn chỉnh (chẳng hạn như giá trị 4 byte phải được căn chỉnh với ranh giới 4 byte). Một số bộ vi xử lý hiện đại thậm chí sẽ gây ra lỗi nếu bạn không tuân thủ các quy tắc này.

Sau hướng dẫn này, bạn được đảm bảo rằng esp nhỏ hơn hoặc bằng giá trị trước đó được liên kết với ranh giới 16 byte.


Tiền tố gs: chỉ có nghĩa là để sử dụng thanh ghi gs phân đoạn để truy cập bộ nhớ hơn là mặc định.

Hướng dẫn mov %eax,-0xc(%ebp) nghĩa là lấy nội dung của thanh ghi ebp, trừ 12 (0xc) và sau đó đặt giá trị eax vào vị trí bộ nhớ đó.


Giải thích về mã. Hàm function của bạn về cơ bản là một không lớn. Việc lắp ráp tạo ra được giới hạn để thiết lập khung stack và teardown, cùng với một số kiểm tra tham nhũng khung stack trong đó sử dụng các vị trí bộ nhớ %gs:14 được đề cập ở trên.

Nó tải giá trị từ vị trí đó (có thể giống như 0xdeadbeef) vào khung ngăn xếp, thực hiện công việc của mình, sau đó kiểm tra ngăn xếp để đảm bảo nó không bị hỏng.

Công việc của nó, trong trường hợp này, là không có gì. Vì vậy, tất cả những gì bạn thấy là công cụ quản trị hàm.

Thiết lập ngăn xếp xảy ra giữa function+0function+12. Tất cả mọi thứ sau đó là thiết lập mã trả về trong eax và rách xuống khung ngăn xếp, bao gồm cả kiểm tra tham nhũng.

Tương tự, main bao gồm thiết lập khung ngăn xếp, đẩy thông số cho function, gọi function, rách khung ngăn xếp và thoát.

Comments đã được chèn vào mã bên dưới:

0x08048428 <main+0>: push %ebp     ; save previous value. 
0x08048429 <main+1>: mov %esp,%ebp   ; create new stack frame. 
0x0804842b <main+3>: and $0xfffffff0,%esp  ; align to boundary. 
0x0804842e <main+6>: sub $0x10,%esp   ; make space on stack. 

0x08048431 <main+9>: movl $0x3,0x8(%esp)  ; push values for function. 
0x08048439 <main+17>: movl $0x2,0x4(%esp) 
0x08048441 <main+25>: movl $0x1,(%esp) 
0x08048448 <main+32>: call 0x8048404 <function> ; and call it. 

0x0804844d <main+37>: leave      ; tear down frame. 
0x0804844e <main+38>: ret       ; and exit. 

0x08048404 <func+0>: push %ebp     ; save previous value. 
0x08048405 <func+1>: mov %esp,%ebp   ; create new stack frame. 
0x08048407 <func+3>: sub $0x28,%esp   ; make space on stack. 
0x0804840a <func+6>: mov %gs:0x14,%eax  ; get sentinel value. 
0x08048410 <func+12>: mov %eax,-0xc(%ebp)  ; put on stack. 

0x08048413 <func+15>: xor %eax,%eax   ; set return code 0. 

0x08048415 <func+17>: mov -0xc(%ebp),%eax  ; get sentinel from stack. 
0x08048418 <func+20>: xor %gs:0x14,%eax  ; compare with actual. 
0x0804841f <func+27>: je  <func+34>   ; jump if okay. 
0x08048421 <func+29>: call <_stk_chk_fl>  ; otherwise corrupted stack. 
0x08048426 <func+34>: leave      ; tear down frame. 
0x08048427 <func+35>: ret       ; and exit. 

Tôi nghĩ lý do cho %gs:0x14 có thể thấy rõ từ trên cao, nhưng chỉ trong trường hợp, tôi sẽ xây dựng ở đây.

Nó sử dụng giá trị này (một sentinel) để đặt trong khung ngăn xếp hiện tại để, một cái gì đó trong hàm làm điều gì đó ngớ ngẩn như viết 1024 byte vào mảng 20 byte được tạo trên ngăn xếp hoặc trong trường hợp của bạn:

char buffer1[5]; 
strcpy (buffer1, "Hello there, my name is Pax."); 

thì trọng điểm sẽ được ghi đè và kiểm tra vào cuối của hàm sẽ phát hiện rằng, gọi hàm thất bại để cho bạn biết, và sau đó có thể hủy để tránh bất kỳ vấn đề khác.

Nếu nó đặt 0xdeadbeef vào stack và điều này đã được đổi thành cái gì khác, sau đó một xor với 0xdeadbeef sẽ tạo ra một giá trị khác không được phát hiện trong các mã với hướng dẫn je.

Các bit có liên quan được diễn giải ở đây:

  mov %gs:0x14,%eax  ; get sentinel value. 
      mov %eax,-0xc(%ebp) ; put on stack. 

      ;; Weave your function 
      ;; magic here. 

      mov -0xc(%ebp),%eax ; get sentinel back from stack. 
      xor %gs:0x14,%eax  ; compare with original value. 
      je  stack_ok   ; zero/equal means no corruption. 
      call stack_bad   ; otherwise corrupted stack. 
stack_ok: leave     ; tear down frame. 
+13

+1: câu trả lời chi tiết đầy ấn tượng :) –

+0

Cảm ơn pax cho câu trả lời chi tiết. Tôi vẫn không hiểu logic đằng sau, sự gia tăng trong địa chỉ, ý tôi là, cách tăng địa chỉ diễn ra, như chính + 0, +1 chính, chính + 3, main + 6, main + 9, main + 17 và như vậy .. – Adi

+0

Vì vậy, đề cập đến điểm của bạn ở trên, strcpy (buffer1, "Xin chào ở đó, tên tôi là Pax."); Bạn có nghĩa là, nó là một loại kiểm tra bộ đệm, mà hệ thống đang làm. Điều đó có nghĩa là lỗ hổng tràn bộ nhớ đệm có thể không được khai thác ở đây. – Adi

3

Pax đã tạo ra một câu trả lời dứt khoát. Tuy nhiên, để hoàn thành, tôi nghĩ rằng tôi muốn thêm một lưu ý về việc GCC chính nó để cho bạn thấy lắp ráp nó tạo ra.

Tùy chọn -S để GCC yêu cầu dừng biên dịch và ghi cụm vào tệp. Thông thường, nó hoặc là chuyển tập tin đó đến bộ lắp ráp hoặc đối với một số mục tiêu ghi trực tiếp tập tin đối tượng.

Đối với mẫu mã trong câu hỏi:

#include <stdio.h> 

void function(int a, int b, int c) { 
    char buffer1[5]; 
    char buffer2[10]; 
} 

void main() { 
    function(1,2,3); 
} 

lệnh gcc -S q3654898.c tạo một file có tên q3654898.s:

 
     .file "q3654898.c" 
     .text 
.globl _function 
     .def _function;  .scl 2;  .type 32;  .endef 
_function: 
     pushl %ebp 
     movl %esp, %ebp 
     subl $40, %esp 
     leave 
     ret 
     .def ___main;  .scl 2;  .type 32;  .endef 
.globl _main 
     .def _main; .scl 2;  .type 32;  .endef 
_main: 
     pushl %ebp 
     movl %esp, %ebp 
     subl $24, %esp 
     andl $-16, %esp 
     movl $0, %eax 
     addl $15, %eax 
     addl $15, %eax 
     shrl $4, %eax 
     sall $4, %eax 
     movl %eax, -4(%ebp) 
     movl -4(%ebp), %eax 
     call __alloca 
     call ___main 
     movl $3, 8(%esp) 
     movl $2, 4(%esp) 
     movl $1, (%esp) 
     call _function 
     leave 
     ret 

Một điều đó là điều hiển nhiên là GCC của tôi (gcc (GCC) 3.4.5 (mingw-vista special r3)) không bao gồm mã kiểm tra ngăn xếp theo mặc định. Tôi tưởng tượng rằng có một tùy chọn dòng lệnh, hoặc rằng nếu tôi đã bao giờ có xung quanh để nudging MinGW của tôi cài đặt lên đến một GCC hiện tại hơn mà nó có thể.

Chỉnh sửa: Nudged làm như vậy bằng Pax, đây là một cách khác để GCC thực hiện nhiều công việc hơn.

 
C:\Documents and Settings\Ross\My Documents\testing>gcc -Wa,-al q3654898.c 
q3654898.c: In function `main': 
q3654898.c:8: warning: return type of 'main' is not `int' 
GAS LISTING C:\DOCUME~1\Ross\LOCALS~1\Temp/ccLg8pWC.s     page 1 


    1       .file "q3654898.c" 
    2       .text 
    3     .globl _function 
    4       .def _function;  .scl 2;  .type 
32;  .endef 
    5     _function: 
    6 0000 55     pushl %ebp 
    7 0001 89E5     movl %esp, %ebp 
    8 0003 83EC28    subl $40, %esp 
    9 0006 C9     leave 
    10 0007 C3     ret 
    11       .def ___main;  .scl 2;  .type 
32;  .endef 
    12     .globl _main 
    13       .def _main; .scl 2;  .type 32; 
.endef 
    14     _main: 
    15 0008 55     pushl %ebp 
    16 0009 89E5     movl %esp, %ebp 
    17 000b 83EC18    subl $24, %esp 
    18 000e 83E4F0    andl $-16, %esp 
    19 0011 B8000000    movl $0, %eax 
    19  00 
    20 0016 83C00F    addl $15, %eax 
    21 0019 83C00F    addl $15, %eax 
    22 001c C1E804    shrl $4, %eax 
    23 001f C1E004    sall $4, %eax 
    24 0022 8945FC    movl %eax, -4(%ebp) 
    25 0025 8B45FC    movl -4(%ebp), %eax 
    26 0028 E8000000    call __alloca 
    26  00 
    27 002d E8000000    call ___main 
    27  00 
    28 0032 C7442408    movl $3, 8(%esp) 
    28  03000000 
    29 003a C7442404    movl $2, 4(%esp) 
    29  02000000 
    30 0042 C7042401    movl $1, (%esp) 
    30  000000 
    31 0049 E8B2FFFF    call _function 
    31  FF 
    32 004e C9     leave 
    33 004f C3     ret 

C:\Documents and Settings\Ross\My Documents\testing> 

Ở đây chúng ta thấy danh sách đầu ra được tạo bởi trình kết hợp. (Tên của nó là GAS, bởi vì nó là phiên bản Gnu của bộ sưu tập * nix cổ điển as. Có một sự hài hước ở đâu đó.)

Mỗi dòng có hầu hết các trường sau: một số dòng, một địa chỉ trong phần hiện tại, byte được lưu trữ tại địa chỉ đó và văn bản nguồn từ tệp nguồn lắp ráp. Các địa chỉ nằm ngoài phần đó của mỗi phần do mô-đun này cung cấp. Mô-đun cụ thể này chỉ có nội dung trong phần .text lưu trữ mã thực thi. Bạn thường sẽ tìm thấy đề cập đến các phần có tên là .data.bss. Rất nhiều tên khác được sử dụng và một số có mục đích đặc biệt. Đọc hướng dẫn cho linker nếu bạn thực sự muốn biết.

+0

'-fstack-protector', tôi tin. Một số bản phân phối Linux bật theo mặc định. – zwol

+0

+1 chỉ cho "Pax đã tạo ra một câu trả lời dứt khoát" :-) Bạn cũng có thể muốn thêm một thực tế là bạn có thể sử dụng 'gcc -Wa, -al ...' để có được trình biên dịch xuất ra một danh sách bao gồm byte được tạo cũng như nguồn. – paxdiablo

+0

@Pax, ;-). Tôi sẽ cố gắng tuyên bố với một khuôn mặt thẳng thắn mà tôi định làm, nhưng MinGW đã làm hỏng đầu ra của tôi và sau đó bữa tối đã sẵn sàng .... thức ăn nấu chín ở nhà được ưu tiên, một cách tự nhiên. – RBerteig

2

Tôi muốn thêm điều đó cho các công cụ đơn giản, đầu ra lắp ráp của GCC thường dễ đọc hơn nếu bạn bật tối ưu hóa một chút. Đây là mã mẫu lần nữa ...

void function(int a, int b, int c) { 
    char buffer1[5]; 
    char buffer2[10]; 
} 

/* corrected calling convention of main() */ 
int main() { 
    function(1,2,3); 
    return 0; 
} 

đây là những gì tôi nhận được mà không tối ưu hóa (OSX 10.6, gcc 4.2.1 + Các bản vá của Apple)

.globl _function 
_function: 
    pushl %ebp 
    movl %esp, %ebp 
    pushl %ebx 
    subl $36, %esp 
    call L4 
"L00000000001$pb": 
L4: 
    popl %ebx 
    leal L___stack_chk_guard$non_lazy_ptr-"L00000000001$pb"(%ebx), %eax 
    movl (%eax), %eax 
    movl (%eax), %edx 
    movl %edx, -12(%ebp) 
    xorl %edx, %edx 
    leal L___stack_chk_guard$non_lazy_ptr-"L00000000001$pb"(%ebx), %eax 
    movl (%eax), %eax 
    movl -12(%ebp), %edx 
    xorl (%eax), %edx 
    je  L3 
    call ___stack_chk_fail 
L3: 
    addl $36, %esp 
    popl %ebx 
    leave 
    ret 
.globl _main 
_main: 
    pushl %ebp 
    movl %esp, %ebp 
    subl $24, %esp 
    movl $3, 8(%esp) 
    movl $2, 4(%esp) 
    movl $1, (%esp) 
    call _function 
    movl $0, %eax 
    leave 
    ret 

Whew, one heck of a mouthful! Nhưng hãy nhìn những gì xảy ra với -O trên dòng lệnh ...

.text 
.globl _function 
_function: 
    pushl %ebp 
    movl %esp, %ebp 
    leave 
    ret 
.globl _main 
_main: 
    pushl %ebp 
    movl %esp, %ebp 
    movl $0, %eax 
    leave 
    ret 

Tất nhiên, bạn làm có nguy cơ mã của bạn được trả lại hoàn toàn không thể nhận ra, đặc biệt là ở các cấp độ tối ưu hóa cao hơn và với các công cụ phức tạp hơn. Ngay cả ở đây, chúng ta thấy rằng cuộc gọi đến function đã bị hủy bỏ là vô nghĩa. Nhưng tôi thấy rằng không cần phải đọc qua hàng chục sự cố tràn không cần thiết thường là đáng giá hơn một chút trầy xước đầu của tôi trên dòng điều khiển.

+0

Nếu nó thậm chí không gọi 'chức năng', tôi tự hỏi tại sao nó bao gồm nó trong thực thi? – Hassan

+0

Nếu 'hàm' được khai báo là' static', hoặc nếu bạn đã biên dịch bằng '-fwhole-program', nó sẽ loại bỏ nó. Nếu không, nó giả định rằng mã bên ngoài đơn vị dịch hiện tại có thể gọi là 'hàm'. – zwol

+0

Ồ đúng. Nhưng sau đó không thể liên kết thả nó? Nếu nó liên kết một tập tin thực thi (không phải là một thư viện), nó sẽ biết các hàm nào đang được gọi từ đâu. Trình liên kết có khả năng bỏ qua mã không? – Hassan

3

Sẽ tốt hơn nếu thử cờ -fno-stack-protector bằng gcc để vô hiệu hóa chim hoàng yến và xem kết quả của bạn.

+0

nhờ berkay..it hoạt động .. – Adi

Các vấn đề liên quan