Saturday, August 8, 2015

Mảng trong C/C++

1. Định nghĩa mảng
Mảng là một khối các ô nhớ liên tục nhau, được chia thành nhiều cell(phần tử), mỗi cell chứa một đối tượng. Các cell sẽ được truy cập bằng chỉ một số index riêng. Cell dầu tiên có chỉ số index là 0, và tăng dần tiếp theo.
Mảng là một kiểu random-access structor bởi vì có thể truy cập bất cứ phần từ nào của mảng ngay lập tức bằng index của nó, cho dù phần tử ở vị trí nào trong mảng thì thời gian truy cập vẫn như nhau.
Có 2 kiểu mảng: mảng tỉnh(static array) và mảng động(dynamic array).
- Mảng tỉnh là mảng có kích thước không đổi.
- Mảng động là mảng có kích thước có thể thay đổi được.
2. Truy cập mảng
Truy cập mảng có thể dùng một trong các cách sau:

int* array = new int[5];   //Khoi tao mang voi do dai la 5
array[2] = 5;              //Gan gia tri 5 vo cell co index 2
array[3] = array[2];       //Lay gia tri cell co index 2 
                           //gan vao cel co index 3
Mảng thường được sử dụng như một con trỏ trỏ vào phần tử đầu tiên(index 0) của mảng. Có thể dùng với các toán tử +, -, ++, -- để trỏ đến một vị trí khác trogn mảng.

int* array = new int[5];   //Khoi tao mang, 
                           //mac dinh con tro array chi vao index 0
array++;                   //Di chuyen con tro 
                           //sang phai 1 phan tu(index 1)
array--;                   //Di chuyen con tro 
                           //sang trai 1 phan tu( ve lai index 0)
array = array + 3;         //Di chuyen con tro 
                           //sang phai 3 phan tu(index 3)
3. Khởi tạo mảng
+ Mảng tỉnh, khoirwtaoj bằng dấu ngoặc vuông []

int arr1[10];              //Tao mang tinh co 10 phan tu.
int arr2[10] = {1, 5, 9};  //Tao mang tinh co 10 phan tu, 
                           //va gan gia tri cho 3 phan tu dau.
+ Mảng động khởi tạo bằng new hoặc malloc

int* arr1 = new int[10];              //Tao mang dong co 10 phan tu.
int* arr2 = new int[10]{1, 5, 4};     //Tao mang dong co 10 phan tu, 
                                      //va gan gia tri cho 3 phan tu dau.
int* arr3 = (int*)malloc(sizeof(int)*10); //Tao mang co 10 phan tu.
Khi khởi tạo vùng nhớ chứa mảng tỉnh nằm trên stack, vùng nhớ chứa mảng động nằm trên heap. Xem new, malloc và calloc
Mảng tỉnh sau khi sử dụng sẽ tự được hủy, mảng động không tự hủy được, phải gọi hàm hủy(nếu khởi tạo bằng new thì dùng delete[], nếu khởi tạo bằng malloc thì dùng free) cho nó.
4. Truyền mảng vào hàm

void func1(int arr[])
{
}

void func2(int* arr)
{
}

func1(array);
func2(array);
2 hàm này không khác gì nhau, chỉ khác về cách khai báo, một hàm dùng dấu [], một hàm dùng dấu * như con trỏ.
Lưu ý: Kích thước(tính theo byte) của mảng tỉnh được tình bằng hàm sizeof, nhưng đối với mảng động thì không tính được, phải lưu lại kích thước của nó khi sử dụng. Mảng tỉnh khi được truyền vào hàm thì trong hàm cũng k0 lấy được kích thước, nên cũng phải truyền theo kích thước vào hàm.

Monday, June 15, 2015

Memory layout of program

Cấu trúc cơ bản của một chương trình C đang chạy trong bộ nhớ(memory) gồm các phần sau:
  • 1. Text segment(code segment)
  • 2. Initialized data(data segment)
  • 3. Uninitialize data(bss segment)
  • 4. Heap
  • 5. Stack

1. Text segment(Code segment)
Text segment(.text segment) hay còn gọi là Code segment là phần bộ nhớ chứa code đã được compile của chương trình, vùng nhớ này thường là read-only. Phần text segment này lấy từ text segment trong file thực thi excutable object.
Thông thường text segment là sharable memory, nó chỉ tồn tại duy nhất một bản copy trên physic memory dùng chung cho tất cả các instance của chương trình. Ví dụ như chương trình soạn thảo văn bản, có thể mở nhiều instance cùng lúc, nhiều process được tạo ra, nhưng chỉ là trên địa chỉ ảo(virtual address) của process, còn thực tế thì chỉ có một bản copy duy nhất trên physic memory.
2. Initalized data segement
Initialized data segment (.data hay còn gọi là Data segment) là vùng nhớ này chứa các biến global, static hoặc extern được khởi tạo trực tiếp trong code bởi lập trình viên(hard-code). Vùng nhớ này có thể được chia làm 2 loại: read-only và read-write. Và có thể một số cấu trúc chia segment này ra thành 2 segment: .data(initialized data) và .rdata(read-only initialized data).
Ví dụ:

char  s1[]  = "Hello world";
char* s2    = "Hello world";
s1[0] = 'A';  //Not error
s2[0] = 'A';  //ERROR

- s1 là non-const array, nghĩa là một mảng chứa 12 character bao gồm cả \0 ở cuối được khởi tạo trong vùng nhớ read-write. Vì s1 là mảng nên địa chỉ của s1 với địa chỉ phần tử đầu tiên của s1 là như nhau(xuất log %p của s1 hay của &s1 đều như nhau). Data của s1 nằm trong vùng nhớ read-write nên được phép thay đổi giá trị.
- s2 là non-const pointer to const data, nghĩa là con trỏ trỏ tới một vùng data(trỏ tới character đầu tiên) và vùng data này nằm trong vùng read-only. s2 chỉ là con trỏ nên địa chỉ của s2 khác với địa chỉ của &s2(&s2 là lấy địa chỉ của địa chỉ hay con trỏ cấp 2). Data của s2 nằm trong vùng nhớ read-only nên không thể thay đổi giá trị.
- Cả 2 data này đều không thể thay đổi kích thước, chỉ có data trên stack và heap là thay đổi được kích thước.

char  s1[]  = "Hello world";
char* s2    = "Hello world";
s1[]  = "Hello world 2";   //ERROR
s2    = "Hello world 2";   //Trỏ con trỏ sang vùng nhớ khác 
                           //chứ không phải thay đổi data ở vùng nhớ củ

3. Uninitialized data segment
Uninitialized data segment (.bss hay còn gọi là BSS segment) là vùng nhớ chứa các biến global, static hoặc extern chưa được khởi tạo trong code và sẽ được khởi tạo bằng 0 khi chương trình bắt đầu thực thi. Các biến này không chiếm bộ nhớ trên object file, mà nó chỉ là một place holder.
4. Stack segment
Stack là vùng nhớ được dùng để chứa các biến local, các biến được truyền đi(passing argument) khi thực thi một hàm và địa chỉ của giá trị trả về sau khi thực thi hàm. Các biến local chỉ tồn tại trong một block code mà nó được định nghĩa, khi ra khỏi block các biến này sẽ được xóa khỏi stack. Các giá trị được thêm vào stack theo nguyên tắc LIFO theo hướng từ địa chỉ cao xuống đia chỉ thấp(trên kiến trúc x86, có thể theo chiều ngược lại ở một số kiến trúc khác).
Thanh nhớ stack pointer sẽ ghi nhớ lại đỉnh của stack mổi khi có giá trị thêm vào. Khi một bộ các giá trị được đẩy vào để thực thi một hàm ta gọi là một stack frame. Một stack frame có ít nhất một địa chỉ trả vể(chứa địa chỉ của giá trị trả về sau khi gọi hàm). Tất cả các hàm gọi lồng nhau được thêm vào stack thành nhiều stack frame. Xem cách hàm được thực thi.
5. Heap segment
Heap là vùng nhớ được cấp phát động bởi các lênh malloc, realloc và free. Vùng nhớ heap được cấp phát mở rộng từ vùng nhớ thấp đến vùng nhớ cao(ngược lại với stack). Stack và heap mở rộng đến khi "đụng" nhau là lúc bộ nhớ cạn kiệt. Vùng nhớ heap là vùng nhớ share giữa các tất cả các shared library và dynamic library được load trong process(tiến trình).

Vùng nhớ của một tiến trình được quản lý bằng địa chỉ ảo(virtual address) nên sẽ liên tục nhau, nhưng thực tế sẽ được ánh xạ không liên tục trên RAM.

Monday, June 1, 2015

Xem code assembly trong Visual Studio

Chức năng xem code assembly của Visual Studio chỉ chạy khi cờ tạo thông tin debug được bật.
- Chỉnh project ở chế độ Debug.
- Đặt break point ở vị trí muốn xem code asm. Chạy project, Visual Studio sẽ dừng lại ở vị trí break point.
- Nhấp phải chuột, chọn Go To Deassembly. Nội dung code asm sẽ hiện ra.
- Code asm sinh ra có thể tùy vào thông tin debug. Có thể chỉnh sang các mode build /Z7, /Zi, /Zl. Ở mode /Zl thông tin debug được tạo ra đầy đủ nên trong code asm sẽ có cả code debug.
Ví dụ: code của một hàm C++ và code asm được sinh ra ở mode /Z7

Tuesday, May 26, 2015

Hàm trong C/C++ được gọi thế nào?

Những đoạn code bên dưới thực thi trên cấu trúc x86(32bit). Xem thêm Deassembly trong Visual Studio

- Calling convention: là một bộ nguyên tắc để gọi hàm, giúp cho việc gọi hàm và các hàm con, đệ quy.. bên trong một chương trình được thực thi bằng các thanh nhớ(register), stack... trên CPU.
- Calling convention:
Nguyên tắc này sử dụng các lệnh cơ bản của CPU: push, pop, call, ret. Có 2 nguyên tắc một dành cho Caller(Caller's rules - code nơi gọi hàm) và Callee(Callee's rules - code trong hàm được gọi). Các ngôn ngữ khác khi tương tác với cấu trúc X86 cũng sẽ tương tự

1. Nguyên tắc cho Caller:
  • 1. Trước khi gọi một hàm/chương trình con(subroutine) caller sẽ lưu lại(backup) các giá trị trên thanh nhớ(register) EBX, ECX, EDX bằng cách đẩy(push) vào stack.
  • 2. Đẩy các tham số của hàm vào stack theo thứ tự từ phải sang trái, tức là tham số cuối cùng sẽ được đẩy vào đầu tiên. Vì stack bắt đầu từ địa chỉ cao và mở rộng xuống địa chỉ thấp khi giá trị được push vào, tham số đầu tiên sẽ nằm ở địa chỉ thấp nhất để khi xử lí sẽ được pop ra đầu tiên.
  • 3. Gọi hàm bằng chỉ thị(instruction) call. Chỉ thị này sẽ đặt địa chỉ của giá trị trả về vào đầu stack và rẽ nhánh vào(nhảy vào) đoạn code bên trong hàm để thực thi.
  • 4. Sau khi thực thi xong hàm, caller sẽ xóa(remove) các tham số của hàm ra khỏi stack, và khôi phục lại top index của stack.
  • 5. Caller có thể lấy giá trị trả về của hàm trong thanh ghi EAX(nếu hàm có trả về).
  • 6. Caller khôi phục(pop ra khỏi stack) các giá trị trên EBX, ECX, EDX đã backup ở bước 1. Như vậy sau khi thực hiện xong, các giá trị trên các thanh nhớ không bị thay đổi bởi hàm/chương trình con.
Ví dụ:

int Add(int num1, int num2)
{
 int m = 0;
 int n = num1 + num2;
 return n;
}

int main()
{
 int a = 10;
 int b = 20;

 int c = Add(a, b);

 return 0;
}
Hình ảnh code Deassembly (trong Visual Studio) của đoạn code trong hàm main(). Ta xem assembly của dòng lệnh gọi hàm Add(phía dưới lệnh gọi hàm Add)
Mô tả code assembly:

int a = 10;
mov         dword ptr [a],0Ah   //Gán giá trị 10(0A ở hệ Hex) vào 
                                //vùng nhớ của biến a
int b = 20;
mov         dword ptr [b],14h   //Gán giá trị 20 vào vùng nhớ của biến b

int c = Add(a, b);
mov         eax,dword ptr [b]   //Đưa trị của biến b vào thanh nhớ EAX
push        eax                 //Đẩy(push) giá trị (của biến b) 
                                // từ thanh nhớ EAX vào stack
mov         ecx,dword ptr [a]   //Đưa trị của biến a vào thanh nhớ EAX
push        ecx                 //Đẩy(push) giá trị (của biến a)
                                // từ thanh nhớ EAX vào stack
call        Add (012811C7h)     //Gọi chỉ thị thực thi hàm Add. 
                                //Sau khi thực thi hàm này, 
                                // kết quả được đẩy vào EAX bởi callee
add         esp,8               //Sau khi thực thi hàm Add xong, xóa(pop)
                                // 2 giá trị của a và b ra khỏi stack
                                // bằng cách di chuyển stack pointer lên 
                                // 8 byte(2 biến int a và b được push vào
                                // mổi biến là 4 byte).
                                //Do stack ghi tử địa chỉ cao 
                                // xuống địa chỉ thấp nên xóa bằng cách
                                // đẩy stack pointer lên.
mov         dword ptr [c],eax   //Gán giá trị trả về từ EAX vào biến c.

2. Nguyên tắc cho Callee:
  • 1. Ở đầu mổi hàm giá trị trên EBP được đẩy vào stack(backup giá trị của EBP), và copy giá trị của ESP và EBP. EBP giống như một chỉ mục để tìm kiếm param và local variable trên stack. EBP giống như một "snapshot" của giá tri trên stack pointer khi hàm con được thực hiện. Các biến local và param nằm ở một vị trí xác định trên stack và EBP sẽ lưu lại vị trí đó.
    
     push ebp
     mov ebp, esp
    
    Phải backup lại giá trị trên EBP trước khi thực hiện hàm để quá trình thực thi nội dung hàm không làm thay đổi giá trị trên EBP của Caller. Sau đó copy giá trị của ESP(stack pointer) vào EBP để sử dụng cho việc truy cập các param và local variable.
  • 2. Cấp phát vùng nhớ trên stack cho biến local. Stack sẽ cấp phát mở rộng xuống vùng nhớ thấp, tùy theo số lượng biến sẽ được cấp phát một kích thước nhất đinh. Ví dụ 3 biến integer(mổi biến 4 byte) sẽ được cấp phát 12byte(stack pointer giảm xuống 12).
    
     sub esp, 12
    
    Cũng như param, biến local nằm trên một vị trí nhất định trên stack và được ghi lại trong EBP(base pointer).
  • 3. Các giá tri trên các thanh nhớ(EDI và ESI) được lưu lại vào stack.
    Sau các bước trên, nội dung hàm sẽ được thực thi.
  • 4. Sau khi hàm kết thúc, kết quả trả về sẽ được lưu vào thanh nớ EAX.
  • 5. Khôi phục các giá trị đã được lưu(backup) trên stack vào lại các thanh nhớ(EDI và ESI).
  • 6. Hủy các biến local bằng cách di chuyển con trỏ stack pointer về lại vị trí trước khi cấp phát vùng nhớ cho các biến này. Giá trị này được lưu trong EBP(ở bước 1).
    
     mov esp, ebp
    
  • 7. Khôi phục lại giá trị EBP đã backup ở bước 1.
  • 8. Thoát ra khỏi hàm và trở về đoạn code caller bằng chỉ thị ret. Chỉ thị này sẽ xóa địa chỉ của biến được return khỏi stack.
Đoạn code assembly của hàm Add trên:
Mô tả đoạn code assembly:

int Add(int num1, int num2)
{
 push        ebp                          //Lưu giá trị base pointer củ
                                          // vào stack
 mov         ebp,esp                      //Gán giá trị base pointer mới
                                          // là giá trị của stack pointer
 sub         esp,8                        //Đẩy stack pointer xuống 8 byte
                                          // (mở rông vùng nhớ cho 2 biến 
                                          // local int là m và n).
 mov         dword ptr [n],0CCCCCCCCh     //Khởi tạo biến
 mov         dword ptr [m],0CCCCCCCCh  

 int m = 0;
 mov         dword ptr [m],0               //Gán giá trị cho biến m.

 int n = num1 + num2;                      //Các dòng lệnh bên dưới 
                                           // để thực hiện phép cộng
 mov         eax,dword ptr [num1]  
 add         eax,dword ptr [num2]  
 mov         dword ptr [n],eax  

 return n;
 mov         eax,dword ptr [n]             //Gán giá trị trả về vào 
                                           // thanh nhớ EAX
}

Hình minh họa stack khi gọi hàm
Stack mở rộng từ trên xuống(địa chỉ cao xuống địa chỉ thấp). Caller sẽ đẩy các giá trị vào stack trước rồi gọi hàm. Khi vào hàm con, các stack của callee được tạo ra trong quá trình xử lí. Khi kết thúc hàm con, callee sẽ xóa các stack của mình(nâng stack pointer lên) và thoát ra khỏi hàm, trở về caller. Caller lấy giá trị trả về và cũng xóa các stack của mình.
Càng nhiều hàm lồng vào nhau thì stack sẽ càng mở rộng xuống để lưu các giá trị của caller bên ngoài. Và khi kết thúc stack cũng được xóa ngược lên.

Friday, May 22, 2015

Sử dụng Constant Reference trong hàm

1. Constant(const)
- Dùng cho biến:  Điều kiện ràng buộc báo cho compiler biết giá trị của biến không được thay đổi.
- Dùng cho hàm: Điều kiện ràng buộc báo cho compiler biết giá trị của class không được thay đổi khi gọi hàm này.
Từ khóa const chỉ có tác dụng với compiler trong quá trình compile. Các thao tác thay đổi giá trị của biến trong lúc runtime(thông qua truy xuất vùng nhớ) điều được.

class A  
{  
public:  
 int m_nValue;  
};  
   
//Su dung  
const A a1;  
A a2;  
a1.m_nValue = 10; //Error  
a2.m_nValue = 10; //NOT error  
  
class B  
{  
public:  
    int m_nValue;  
  
    int Function1() const  
    {  
        m_nValue = 10; //Error  
        return m_nValue;  
    }  
  
    int Function2() const  
    {  
        char* p = (char*)this;  
        (int*)p = 10;     //NOT error. Gán giá trị 10 cho biến m_nValue 
                          //thông qua truy xuất vùng nhớ.  
        return m_nValue;  
    }  
}; 
- Ràng buộc của const tác dụng lên thành phần bên trái của nó, nếu không có thành phần bên trái thì nó sẽ tác dụng lên thành phần bên phải.

int const * nVal = 10  //(1)
const int * nVal = 10; //(2)
int * const nVal = 10; //(3)
void function(int const * const nVal) const; //(4)
  • (1): khai báo biến con trỏ(nVal) trỏ tới giá trị constant integer, nghĩa là KHÔNG được thay đổi giá trị 10. Ở đây const tác dụng vào thành phần bên phải là int vì không có thành phần bên trái nó.
  • (2): ý nghĩa tương tự như (1). Ở đây const tác dụng vào thành phần bên trái là int.
  • (3): khai báo biến constant pointer của kiểu int. Nghĩa là có thể thay đổi giá trị 10 nhưng KHÔNG thể thay đổi con trỏ của biến nVal sang ô nhớ khác.
  • (4): khai báo hàm constant -> KHÔNG được thay đổi giá trị trong lớp chứa hàm. Tham số truyền vào là một constant pointer trỏ tới một constant value -> KHÔNG được thay đổi địa chỉ của con trỏ và KHÔNG được thay đổi giá trị của kiểu int được truyền vào.

2. Reference(&)
- Lấy địa chỉ của biến. - Thường được dùng kết hợp với const khi truyền tham số vào hàm.

int 	n 	= 10;
int* 	pVal 	= &n; //Lay dia chi cua bien n gan vao con tro pVal

*pVal = 	5;      //n cung se bang 5
Ví dụ trên, lấy địa chỉ của biến n gán vào con trỏ pVal hay trỏ con trỏ pVal vào vùng nhớ của biến n. Khi đó nếu thay đổi giá trị của n thì giá trị trong vùng nhớ được con trỏ pVal trỏ tới cũng sẽ thay đổi theo, hay ngược lại.

3. Hàm
- Thực hiện một đoạn code và trả về kết quả.
- Cách hoạt động của hàm trong một class:
+ Hàm không tham số, không kết quả trả về: địa chỉ của hàm và con trỏ this của đối tượng(thực thể của class) được đẩy vào vùng nhớ stack rồi sau đó CPU thực thi hàm với đối tượng this. Sau khi thực thi xong, do không có trả kết quả về(kiểu return void) nên kết thúc quá trình thực hiện, các giá trị được đẩy ra khỏi.
+ Hàm có tham số và kết quả trả về: các biến được, địa chỉ hàm, con trỏ this lần lượt được đẩy vào stack. CPU thực hiện hàm, kết quả trả về được đẩy vào một thanh nhớ trên CPU. Kết thúc hàm các giá trị được đẩy ra khỏi stack.

** Hàm có sử dụng const reference và không sử dụng const reference:

struct Data
{
    int id;
    int Value;
};

Data DoSomething1(Data data); //function signature
Data DoSomething2(const Data& data);

//Su dung
Data data = 10;
Data ret1 = DoSomething1(data);
Data ret2 = DoSomething2(data);
Hàm DoSomething1: không sử dụng constant reference
+ B1: giá trị của biến data được copy vào stack.
+ B2.1: gọi instruction call để thực thi hàm.
+ B2.2: ghi kết quả trả về(kiểu Data) vào thanh nhớ.
+ B2.3: pop các giá trị ra khỏi stack.
+ B3: copy giá trị trả về từ thanh nhớ vào biến ret1.

Hàm DoSomething2: sử dụng constant reference
+ B1: địa chỉ của biến data được copy vào stack.
+ B2.1: gọi instruction call để thực thi hàm.
+ B2.2: ghi kết quả trả về(kiểu Data) vào thanh nhớ.
+ B2.3: pop các giá trị ra khỏi stack.
+ B3: copy giá trị trả về từ thanh nhớ vào biến ret2.

Có sự khác nhau ở bước B1 trong quá trình thực hiện 2 hàm. Ở hàm DoSomething1 thì data được copy vào stack và size của data là 8byte, ở hàm DoSomething2 địa chỉ của data được copy vào stack, size của một địa chỉ trên kiến trúc 32bit là 4byte(32bit). Như vậy nếu struct Data có size lớn (giả sử) khoảng 10Kb thì mổi lần thực hiện hàm DoSomething1 sẽ tốn 10Kb memory khi copy vào stack, trong khi với hàm DoSomething2 thì luôn luôn tốn 4byte. Xem thêm cách CPU thực thi một hàm
Tương tự khi xài const reference cho biến trả về của hàm, thì hàm cũng sẽ trả về một địa chỉ thay vì cả data, câu lệnh chạy nhanh hơn.

const Data& DoSomething3();
Hàm DoSomething3 trả về là một const reference. Khi sử dụng kiểu trả về này cần lưu ý, giá trị trong biến trả về KHÔNG được là biến local của hàm. Vì biến local sau khi ra khỏi hàm sẽ bị hủy và kết quả trả về không hợp lệ.

const Data& DoSomething3()
{
    Data data;
    return data; //Error! data là biến local, sẽ bị hủy sau khi ra khỏi hàm
}

Như vậy truyền địa chỉ khi sử dụng hàm sẽ tiết kiệm được memory. Nhưng khi truyền địa chỉ, giá trị của biến truyền vào có thể bị thay đổi trong hàm nên sử dụng kèm const đê không cho thay đổi giá trị lúc thực thi hàm