Nội dung
Trong bài viết này, mình sẽ giới thiệu về bộ cục điển hình của một chương trình C. Nó bao gồm các phân vùng sau:
- Phân vùng văn bản (Text Segment)
- Phân vùng dữ liệu đã khởi tạo (Initialized Data Segment)
- Phân vùng dữ liệu chưa khởi tạo (Uninitialized Data Segment)
- Phân vùng Heap
- Phân vùng Stack
Phân vùng văn bản
Phân vùng văn bản, còn được gọi là phân vùng mã nguồn, là một trong những thành phần của chương trình trong tập tin hoặc trong bộ nhớ, chứa các chỉ thị thực thi. Vì là một vùng trong bộ nhớ, phân vùng văn bản có thể được đặt bên dưới heap hoặc stack để ngăn việc heap và stack tràn ra và ghi đè lên nó. Thông thường, phân vùng văn bản có thể chia sẻ sao cho chỉ một bản sao duy nhất cần có trong bộ nhớ cho các chương trình được thực thi thường xuyên, chẳng hạn như trình soạn thảo văn bản, trình biên dịch C, shell, v.v. Ngoài ra, phân vùng văn bản thường là phân vùng chỉ đọc (read-only), để ngăn chương trình vô tình sửa đổi các chỉ thị của nó.
Phân vùng dữ liệu đã khởi tạo
Phân vùng dữ liệu được khởi tạo, thường được gọi đơn giản là phân vùng dữ liệu. Phân vùng này là một phần của không gian địa chỉ ảo của chương trình, chứa các biến toàn cục và biến tĩnh được tạo bởi người lập trình.
Lưu ý rằng phân vùng dữ liệu không phải là phân vùng chỉ đọc, do đó các giá trị của các biến có thể được thay đổi trong lúc thực thi. Phân vùng này có thể được phân loại thành phân vùng chỉ đọc hay đọc-ghi (read-write) tùy vào yêu cầu của người lập trình.
Chẳng hạn, ta định nghĩa một biến string toàn cục là string s[] = “Hello world” và một phát biểu như int debug = 1 bên ngoài hàm main. Biến s và debug sẽ được lưu trữ trong phân vùng đọc-ghi. Còn nếu ta khai báo const char* s = “Hello world”, thì khi đó “Hello world” sẽ nằm trong vùng chỉ đọc và con trỏ s nằm trong vùng đọc-ghi.
Một ví dụ khác, cả hai phát biểu static int i = 10 và int i = 10 (nằm ngoài hàm main) đều được lưu trong phân vùng dữ liệu.
Phân vùng dữ liệu chưa khởi tạo
Phân vùng dữ liệu chưa được khởi tạo, thường được gọi là phân vùng dữ liệu bss, được đặt theo tên của một toán tử của trình biên dịch mã cổ đại, là viết tắt của “block started by symbol”. Dữ liệu trong phân vùng này được kernel khởi tạo thành số học 0 trước khi chương trình bắt đầu thực thi.
Dữ liệu chưa được khởi tạo nằm ở cuối phân vùng dữ liệu và chứa tất cả các biến toàn cục và biến tĩnh được khởi tạo là 0 hoặc không có khởi tạo rõ ràng trong mã nguồn.
Ví dụ, khi một biến được khai báo là static int i; hoặc một biến toàn cục int i; sẽ được lưu trong vùng nhớ bss.
Phân vùng Heap
Heap là phân vùng nơi việc cấp phát bộ nhớ động diễn ra. Heap bắt đầu ở cuối phân vùng bss và kích thước tăng lên với các địa chỉ lớn hơn từ đó. Phân vùng heap được quản lý bởi các hàm malloc, realloc và free, có thể sử dụng các lệnh gọi hệ thống brk và sbrk để điều chỉnh kích thước của nó (lưu ý rằng việc sử dụng brk/sbrk hay chỉ một vùng heap duy nhất, không bắt buộc phải thỏa các điều kiện của malloc/realloc/free, chúng cũng có thể được thực hiện bằng cách sử dụng mmap để dành vị trí cho các vùng bộ nhớ ảo không tiếp giáp vào không gian địa chỉ ảo của chương trình). Heap được chia sẻ bởi tất cả các thư viện dùng chung và các module được tải động trong một chương trình.
Phân vùng Stack
Theo truyền thống, phân vùng ngăn xếp (stack) liền kề phân vùng heap và kích thước tăng lên theo hướng ngược lại (địa chị nhỏ dần); khi con trỏ stack gặp con trỏ heap, tức bộ nhớ trống đã cạn kiệt. Với không gian địa chỉ lớn hiện nay và kỹ thuật bộ nhớ ảo, chúng có thể được đặt ở hầu hết mọi nơi, nhưng chúng vẫn thường phát triển theo hướng ngược lại so với Heap.
Phân vùng stack chứa ngăn xếp cho chương trình theo cấu trúc LIFO. Trên kiến trúc máy tính PC x86 tiêu chuẩn, nó phát triển theo địa chỉ 0; trên một số kiến trúc khác nó phát triển theo hướng ngược lại. Một con trỏ ngăn xếp nằm trên đỉnh của ngăn xếp và được điều chỉnh mỗi khi một giá trị được đẩy vào stack. Tập hợp các thông tin cho một lời gọi hàm được đẩy vào stack gọi là khung ngăn xếp. Một khung ngăn xếp bao gồm ít nhất là địa chỉ trả về nơi mà hàm được gọi.
Ngăn xếp, nơi lưu trữ các biến tự động cũng như các thông tin mỗi khi hàm được gọi. Mỗi khi một hàm được gọi, địa chỉ nơi quay trở lại và một số thông tin nhất định về môi trường của hàm gọi được lưu trên ngăn xếp. Hàm mới được gọi sau đó phân bổ chỗ trên ngăn xếp cho các biến tự động và tạm thời của nó. Đây là cách các hàm đệ quy trong C hoạt động. Mỗi khi một hàm đệ quy gọi chính nó, một khung ngăn xếp mới được sử dụng, do đó, một tập hợp các biến của nó không can thiệp vào các biến từ một hàm đệ quy gọi nó.
Ví dụ minh họa
Lệnh size cho ta biết thông tin về kích thước (bytes) của phân vùng text, data và bss của một chương trình. Chi tiết về lệnh này có thể xem thêm trong man page của nó. Bây giờ, hãy thử kiểm tra một chương trình C bên dưới,
Lưu ý, kích thước của chương trình có thể khác nhau tùy vào phiên bản của hệ điều hành cũng như phiên bản và cách tối ưu của trình biên dịch.
#include <stdio.h>
int main(void)
{
return 0;
}
$ gcc memory-layout.c -o memory-layout
$ size memory-layout
text data bss dec hex filename
960 248 8 1216 4c0 memory-layout
Hãy thêm một biến toàn cục vào chương trình và kiểm tra kích thước bss.
#include <stdio.h>
int global; /* Uninitialized variable stored in bss*/
int main(void)
{
return 0;
}
$ gcc memory-layout.c -o memory-layout
$ size memory-layout
text data bss dec hex filename
960 248 12 1220 4c4 memory-layout
Một biến tĩnh khi được thêm vào làm tăng kích thước của bss.
#include <stdio.h>
int global; /* Uninitialized variable stored in bss*/
int main(void)
{
static int i; /* Uninitialized static variable stored in bss */
return 0;
}
$ gcc memory-layout.c -o memory-layout
$ size memory-layout
text data bss dec hex filename
960 248 16 1224 4c8 memory-layout
Bây giờ, hãy thử khởi tạo giá trị cho biến static, nó sẽ được lưu trong phân vùng dữ liệu.
#include <stdio.h>
int global; /* Uninitialized variable stored in bss*/
int main(void)
{
static int i = 100; /* Initialized static variable stored in DS*/
return 0;
}
$ gcc memory-layout.c -o memory-layout
$ size memory-layout
text data bss dec hex filename
960 252 12 1224 4c8 memory-layout
Tương tự cho trường hợp khởi tạo giá trị cho biến toàn cục. Khi đó, nó sẽ lưu trong phân vùng dữ liệu.
#include <stdio.h>
int global = 10; /* initialized global variable stored in DS*/
int main(void)
{
static int i = 100; /* Initialized static variable stored in DS*/
return 0;
}
$ gcc memory-layout.c -o memory-layout
$ size memory-layout
text data bss dec hex filename
960 256 8 1224 4c8 memory-layout
Để lại một bình luận