close up photo of programming of codes

codecungnhau.com

Một trang web về kỹ thuật lập trình

Bố cục bộ nhớ của một chương trình C

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:

  1. Phân vùng văn bản (Text Segment)
  2. Phân vùng dữ liệu đã khởi tạo (Initialized Data Segment)
  3. Phân vùng dữ liệu chưa khởi tạo (Uninitialized Data Segment)
  4. Phân vùng Heap
  5. Phân vùng Stack
Bố cục bộ nhớ điển hình của chương trình đang chạy

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 = 10int 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

Đã đăng vào

trong

bởi

Thẻ:

Bình luận

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *