Bài viết này mình chỉ dụng Microsoft Studio Visual C++ 2010 làm công cụ. Nào hãy cùng bắt đầu với một chương trình đơn giản: Hello World program.
#include <stdio.h>
int main()
{
printf("hello, world");
return 0;
};
Mình compile nó trong MSVC 2010: cl 1.cpp /Fa1.asm
(/Fa option có nghĩa là sinh ra assembly listing file)
CONST SEGMENT
$SG3830 DB ’hello, world’, 00H
CONST ENDS
PUBLIC _main
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_main PROC
push ebp
mov ebp, esp
push OFFSET $SG3830
call _printf
add esp, 4
xor eax, eax
pop ebp
ret 0
_main ENDP
_TEXT ENDS
Note: Đoạn code trên đã được tinh giản đi những thông tin không cần thiết để phù hợp với kích thước bài viết.
Trong trường hợp này chúng ta có 2 phân đoạn (segment) : CONST (dành cho dữ liệu) và _TEXT(dành cho code). Chuỗi (string) "hello, world " trong C/C++ có kiểu const char*, tuy nhiên nó không có một danh định riêng.
Vì compiler cần làm việc với string này, vì vậy nó định nghĩa một cái tên nội bộ (internal name) cho string cần thao tác trong trường hợp này đó là $SG3830, các bạn không cần phải lo lắng về cái tên này.
Như các bạn có thể nhìn thấy string được kết thúc bởi zero byte - đó là quy định chuẩn cho strings của C/C++.
Trong code segment _TEXT chỉ xuất hiện một hàm đó là _main.
Hàm _main bắt đầu với code mào đầu (prologue code) và kết thúc với code kết thúc (epilogue code), giống như bất cứ hàm nào khác. Tôi sẽ nói về phần mào đầu và kết thúc ở phía dưới, hiện tại chúng ta cứ chấp nhận như vậy đã.
Sau phần mở đầu chúng ta có thể nhìn thấy lời gọi printf(): CALL _printf. (Nếu tới đây bạn thấy khó hiểu thì hãy quay lại loạt tut, tại tut7 có giới thiệu lệnh CALL)
Trước khi thực hiện lời gọi, địa chỉ của string (hay là một con trỏ tới nó) chứa lời chào mừng của chúng ta được đặt vào trong stack với sự giúp đỡ của lệnh PUSH (kiến thức về lệnh PUSH cũng như stack có tại tut4).
Khi hàm printf() trả luồng điều khiển về hàm main(), địa chỉ string (hay là một con trỏ đến nó) vẫn còn nằm trên stack.
Bởi vì chúng ta không cần nó (địa chỉ string ) nữa, nên chúng ta chỉnh sửa lại con trỏ ngăn xếp(ESP) để điều chỉnh lại đỉnh ngăn xếp.
ADD ESP, 4 nghĩa là cộng 4 vào thanh ghi ESP.
Tại sao lại là 4? bởi vì nó là 32-bit code, chúng ta cần chính xác 4 bytes để biểu diễn địa chỉ để đưa vào stack. Nó là 8 bytes trong x64-code.
"ADD ESP, 4" là tương đương với "POP register" nhưng không sử dụng bất cứ thanh ghi nào.
Intel C++ compiler sử dụng POP ECX bởi vì opcode của lệnh này là ngắn hơn ADD ESP, x (1 so với 3), cái này có thể dễ dàng của tra bằng cách search opcode trên internent hoặc có thể load trực tiếp vào OllyDbg để thấy được opcode của chúng.
Sau printf() call, trong code C/C++ ban đầu là return 0 - trả về zero như là kết quả của hàmmain().
Lệnh biểu diễn return 0 trong assemby đó là XOR EAX, EAX
XOR thường được sử dụng bởi compiler thay thế cho lệnh MOV EAX, 0 bởi vì opcode cua nó ngắng hơn (2 so với 5)
Môt vài compilers sử dụng lệnh SUB EAX, EAX, có nghĩa là trừ giá trị của EAX cho EAX, trong bất cứ trường hợp nào thì kết quả cũng là 0.
Lệnh cuối cùng là lệnh RET trả luồng điều khiển về hàm gọi trong trường hợp này là main().
Phần mào đầu và kết thúc
Mào đầu của một hàm (function prologue) là các lệnh nằm ở phần bắt đầu của hàm. Nó thường giống như sau:
push ebp
mov ebp, esp
sub esp, X
Những lệnh trên làm gì? Chúng lưu giá trị của thanh ghi EBP, set EBP đến ESP và sau đó cấp phát không gian trong stack cho các biến cục bộ.
Giá trị EBP được cố định đến một khoảng thực thi của hàm và nó được sử dụng cho việc truy nhập đến các biến cục bộ và arguments. Bạn cũng có thể sử dụng ESP, nhưng nó thay đổi liên tục và không thích hợp.
Phần kết hàm giải phóng không gian đã được cấp phát trong stack, trả lại giá trị EBP vể trạng thái ban đầu và trả luồng điều khiển về hàm gọi:
mov esp, ebp
pop ebp
ret 0
http://iamtoet.blogspot.com/
No comments:
Post a Comment