You are on page 1of 14

Mục Lục

I. Bộ nhớ đệm (cache) .................................................................................................................... 2

1. Giới thiệu và phân loại................................................................................................................ 2

2. Nguyên lý hoạt động. .................................................................................................................. 3

a. Cấu trúc cache và bộ nhớ chính: ............................................................................................... 3

b. Các phương pháp ánh xạ. ......................................................................................................... 4

c. Các thuật giải thay thế. ............................................................................................................. 6

d. Phương pháp ghi dữ liệu khi cache hit. ..................................................................................... 6

II. Ngăn xếp. .................................................................................................................................... 7

1. Sơ đồ tổ chức bộ nhớ của một chương trình. .............................................................................. 7

2. PUSH và POP ............................................................................................................................. 8

III. Vấn đề “tràn bộ đệm” trong ngôn ngữ C. ............................................................................... 10

1. Tràn bộ nhớ đệm trên stack ...................................................................................................... 10

2. Ngôn ngữ lập trình và tràn bộ nhớ đệm .................................................................................... 12

3. Chống tràn bộ nhớ đệm trên stack............................................................................................. 13

Kết Luận: ............................................................................................................................................. 14

1
Đề tài: Tìm hiểu về phương thức quản lý bộ nhớ đệm, ngăn xếp
trong các HĐH xây dựng trên ngôn ngữ C.
Trong môn học hề điều hành, các giáo trình đều nhắc những khái niệm như
là: kĩ thuật phòng đệm, vùng đệm, spool, cache,… Nhưng nhìn chung chỉ mới
dừng lại ở những giới thiệu chức năng, mô tả nguyên lý chung,…

Trong đề tài này, chúng em được giao tìm hiểu cụ thể về vấn đề liên quan
đến các vùng đệm, đó là bộ nhớ đệm và ngăn xếp hệ thống. Các nội dung bên dưới
đây sẽ tập trung tìm hiểu về chức năng, cách hoạt động, các đặc điểm, khai thác
các vấn đề liên quan trong việc lập trình.

I. Bộ nhớ đệm (cache)


1. Giới thiệu và phân loại.

Ngày nay, chúng ta khi tìm hiểu về CPU thường nghe đến những thuật ngữ
như là L1 Cache, L2 Cache, … Cache là tên gọi của bộ nhớ đệm, có chức năng làm
tăng tốc độ xử lý. Nói một cách bài bản, cache là một cơ chế lưu trữ tốc độ cao đặc
biệt. Nó có thể là một vùng lưu trữ của bộ nhớ chính hay một thiết bị lưu trữ tốc độ
cao độc lập.

Đặc điểm:

- Cache có tốc độ nhanh hơn bộ nhớ chính


- Cache được đặt giữa CPU và bộ nhớ chính nhằm tăng tốc độ CPU truy cập
bộ nhớ
- Cache có thể được đặt trên chip CPU

Phân loại:

Có hai dạng lưu trữ cache được dùng phổ biến trong máy tính cá nhân là
memory caching (bộ nhớ cache hay bộ nhớ truy xuất nhanh) và disk caching (bộ
nhớ đệm đĩa).

* Memory cache: Đây là một khu vực bộ nhớ được tạo bằng bộ nhớ tĩnh (SRAM)
có tốc độ cao nhưng đắt tiền thay vì bộ nhớ động (DRAM) có tốc độ thấp hơn và rẻ
hơn, được dùng cho bộ nhớ chính. Cơ chế lưu trữ bộ nhớ cahce này rất có hiệu
quả. Bởi lẽ, hầu hết các chương trình thực tế truy xuất lặp đi lặp lại cùng một dữ
liệu hay các lệnh như nhau. Nhờ lưu trữ các thông tin này trong SRAM, máy tính
sẽ không phải truy xuất vào DRAM vốn chậm chạp hơn.

2
* Disk cache: Bộ nhớ đệm đĩa cũng hoạt động cùng nguyên tắc với bộ nhớ cache,
nhưng thay vì dùng SRAM tốc độ cao, nó lại sử dụng ngay bộ nhớ chính. Các dữ
liệu được truy xuất gần đây nhất từ đĩa cứng sẽ được lưu trữ trong một buffer
(phần đệm) của bộ nhớ. Khi chương trình nào cần truy xuất dữ liệu từ ổ đĩa, nó sẽ
kiểm tra trước tiên trong bộ nhớ đệm đĩa xem dữ liệu mình cần đang có sẵn không.
Cơ chế bộ nhớ đệm đĩa này có công dụng cải thiện một cách đáng ngạc nhiên sức
mạnh và tốc độ của hệ thống. Bởi lẽ, việc truy xuất 1 byte dữ liệu trong bộ nhớ
RAM có thể nhanh hơn hàng ngàn lần nếu truy xuất từ một ổ đĩa cứng.

2. Nguyên lý hoạt động.

- CPU yêu cầu nội dung của ngăn nhớ.


- CPU kiểm tra trên cache với dữ liệu này.
- Nếu có, CPU nhận dữ liệu từ cache (nhanh).
- Nếu không có, đọc Block nhớ chứa dữ liệu từ bộ nhớ chính vào cache.
- Tiếp đó chuyển dữ liệu từ cache vào CPU.

a. Cấu trúc cache và bộ nhớ chính:

- Bộ nhớ chính có 2N byte nhớ.


- Bộ nhớ chính và cache được chia thành các khối có kích thước bằng nhau:

• Bộ nhớ chính: B0, B1, B2, ... , Bp-1 (p Blocks)


• Bộ nhớ cache: L0, L1, L2, ... , Lm-1 (m Lines)

3
• Kích thước của Block = 8,16,32,64,128 byte

- Một số Block của bộ nhớ chính được nạp vào các Line của cache.
- Nội dung Tag (thẻ nhớ) cho biết Block nào của bộ nhớ chính hiện đang được
chứa ở Line đó.
- Khi CPU truy nhập (đọc/ghi) một từ nhớ, có hai khả năng xảy ra:

• Từ nhớ đó có trong cache (cache hit).


• Từ nhớ đó không có trong cache (cache miss).

b. Các phương pháp ánh xạ.

Các phương pháp ánh xạ chính là các phương pháp tổ chức của cache.

- Ánh xạ trực tiếp (Direct mapping).


- Ánh xạ liên kết toàn phần (Fully associative mapping).
- Ánh xạ liên kết tập hợp (Set associative mapping).

Ánh xạ trực tiếp: Mỗi Block của bộ nhớ chính chỉ có thể được nạp

vào một Line của cache.

Đặc điểm: bộ so sánh đơn giản nhưng xác suất cache hit thấp.

4
Ánh xạ liên kết toàn phần: Mỗi Block có thể nạp vào bất kỳ Line

nào của cache.

Đặc điểm: Xác suất cache hit cao nhưng bộ so sánh phức tạp.

Ánh xạ liên kết tập hợp: Cache đươc chia thành các Tập (Set), mỗi một Set chứa
một số Line.

Đặc điểm: kết hợp 2 phương pháp trước.

5
c. Các thuật giải thay thế.

Đối với các dữ liệu đã nằm lâu trong cache, cần có phương pháp để thay thế
các dữ liệu trong cache 1 cách hợp lý.

Đối với ánh xạ liên kêt có các giải thuật thay thế sau:

- Được thực hiện bằng phần cứng (nhanh).


- Random: Thay thế ngẫu nhiên.
- FIFO (First In First Out): Thay thế Block nào nằm lâu nhất ở trong Set đó
- LFU (Least Frequently Used): Thay thế Block nào trong Set có số lần truy
nhập ít nhất trong cùng một khoảng thời gian.
- LRU (Least Recently Used): Thay thế Block ở trong Set tương ứng có thời
gian lâu nhất không được tham chiếu tới.
- Tối ưu nhất: LRU.

d. Phương pháp ghi dữ liệu khi cache hit.

- Ghi xuyên qua (Write-through):

6
• Ghi cả cache và cả bộ nhớ chính.
• Tốc độ chậm.

- Ghi trả sau (Write-back):

• Chỉ ghi ra cache.


• Tốc độ nhanh.
• Khi Block trong cache bị thay thế cần phải ghi trả cả Block về bộ nhớ
chính.

II. Ngăn xếp.


Mọi lập trình viên đều biết đến ngăn xếp là một cấu trúc dữ liệu với nguyên
tắc LIFO (Last In First Out). Cấu trúc dữ liệu này được các hệ điều hành sử dụng
trong việc cấp phát tài nguyên nhớ cho các chương trình.

Để tìm hiểu vấn đề này, trước hết, hãy xem xét sơ đồ tổ chức bộ nhớ của
một chương trình.

1. Sơ đồ tổ chức bộ nhớ của một chương trình.


/------------------\ địa chỉ vùng nhớ cao
| |
| Stack |
| |
|------------------|
| (Initialized) |
| Data |
| (Uninitialized) |
|------------------|
| |
| Text |
| |
\------------------/ địa chỉ vùng nhớ thấp

Khi một chương trình chạy cần các vùng nhớ cho hàm, các biến cục bộ,
biến toàn cục. Hệ điều hành quản lý việc cấp phát này thông qua 2 vùng nhớ là
stack và heap:

- Heap là vùng nhớ dùng để cấp phát cho các biến toàn cục hoặc các vùng
nhớ được cấp phát bằng hàm malloc().
- Stack là vùng nhớ dùng để lưu các tham số và các biến cục bộ của hàm.

7
Các biến trên heap được cấp phát từ vùng nhớ thấp đến vùng nhớ cao.
Trên stack thì hoàn toàn ngược lại, các biến được cấp phát từ vùng nhớ cao đến
vùng nhớ thấp.

Stack hoạt động theo nguyên tắc "vào sau ra trước"(Last In First Out -
LIFO). Các giá trị được đẩy vào stack sau cùng sẽ được lấy ra khỏi stack trước
tiên.

2. PUSH và POP

Stack đổ từ trên xuống duới(từ vùng nhớ cao đến vùng nhớ thấp). Thanh ghi ESP
luôn trỏ đến đỉnh của stack(vùng nhớ có địa chỉ thấp).
đỉnh của bộ nhớ /------------\ đáy của stack
| |
| |
| |
| |
| |
| | <-- ESP
đáy của bộ nhớ \------------/ đỉnh của stack

* PUSH một value vào stack


đỉnh của bộ nhớ /------------\ đáy của stack
| |
| |
| |
| |
| | <- ESP cũ
|------------|
(2) -> value | <- ESP mới = ESP cũ - sizeof(value) (1)
đáy của bộ nhớ \------------/ đỉnh của stack

1/ESP=ESP-sizeof(value).
2/ value được đẩy vào stack

* POP một value ra khỏi stack


đỉnh của bộ nhớ /------------\ đáy của stack
| |
| |
| |
| |
| | <- ESP mới = ESP cũ + sizeof(value)(2)
|------------|
(1) <- value | <- ESP cũ
đáy của bộ nhớ \------------/ đỉnh của stack

8
1/ value được lấy ra khỏi stack
2/ ESP=ESP+sizeof(value)

3. Cách làm việc của hàm.

- Thanh ghi EIP luôn trỏ đến địa chỉ của câu lệnh tiếp theo cần thi hành.
- Khi gọi hàm, đầu tiên các tham số được push vào stack theo thứ tự ngược
lại. Tiếp theo địa chỉ của câu lệnh được push vào stack. Sau đó, thanh ghi
EBP được push vào stack(dùng để lưu giá trị cũ của EBP).
- Khi kết thúc hàm, thanh ghi EBP được pop ra khỏi stack(phục hồi lại giá trị
cũ của EBP). Sau đó địa chỉ trở về(ret address) được pop ra khỏi stack và
lệnh tiếp theo sau lời gọi hàm sẽ được thi hành.
- Thanh ghi EBP được dùng để xác định các tham số và các biến cục bộ của
hàm.

* Ví dụ:
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}

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

Chương trình trên được viết bằng ngôn ngữ C, để tìm hiểu, ta compile đoạn
mã trên, hệ điều hành linux. Quan sát đoạn mã assembly tương ứng ta thấy:
pushl $3
pushl $2
pushl $1
call function

3 tham số truyền cho function() lần lượt được push vào stack theo thứ tự
ngược lại. Câu lệnh 'call' sẽ push con trỏ lệnh(tức là thanh ghi EIP) vào stack để
lưu địa chỉ trở về.

Các lệnh đầu tiêu trong hàm function() sẽ có dạng như sau:
pushl %ebp
movl %esp,%ebp
subl $20,%esp

9
Đầu tiên ESP(frame pointer) được push vào stack. Sau đó chương trình copy
ESP vào EBP để tạo một FP pointer mới. Bạn dễ nhận thấy lúc này ESP và EBP
đều đang trỏ đến ô nhớ chứa EBP cũ. Hãy ghi nhớ điều này. Tiếp theo ESP được
trừ đi 20 để dành không gian cho các biến cục bộ của hàm function().

Vì chương trình 32 bits nên 5 bytes buffer1 sẽ là 8 bytes(2 words) trong bộ


nhớ(do làm tròn đến 4 bytes hay là 32 bits), 10 bytes buffer2 sẽ là 12 bytes trong
bộ nhớ(3 words). Tổng cộng sẽ tốn 8+12=20 bytes cho các biến cục bộ của
function() nên ESP phải bị trừ đi 20! Stack sẽ có dạng như sau:
đáy của đỉnh của
bộ nhớ bộ nhớ
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]

đỉnh của 12 bytes 8 bytes 4b 4b đáy của


stack stack

Trong hàm function(), nội dung thanh ghi EBP không bị thay đổi.

Khi kết thúc hàm function():


movl %ebp,%esp
popl %ebp
ret

Chương trình copy EBP vào ESP. Vì EBP khi bắt đầu hàm trỏ đến ô nhớ
chứa EBP cũ và EBP không bị thay đổi trong hàm function() nên sau khi thực hiện
lệnh movl, ESP sẽ trỏ đến ô nhớ chứa EBP cũ. Chương trình sẽ phục hồi lại giá trị
cũ cho EBP đồng thời ESP sẽ bị giảm 4(ESP=ESP-sizeof(EBP cũ)) sau lệnh popl.
Như vậy ESP sẽ trỏ đến ô nhớ chứa địa chỉ trở về(nằm ngay trên ô nhớ chứa EBP
cũ). ret sẽ pop địa chỉ trở về ra khỏi stack, ESP sẽ bị giảm 4 và chương trình tiếp
tục thi hành câu lệnh sau lệnh call function().

III. Vấn đề “tràn bộ đệm” trong ngôn ngữ C.


Trong các lĩnh vực an ninh máy tính và lập trình, một lỗi tràn bộ nhớ đệm
hay gọi tắt là lỗi tràn bộ đệm là một lỗi lập trình có thể gây ra một ngoại lệ truy
nhập bộ nhớ máy tính và chương trình bị kết thúc, hoặc khi người dùng có ý phá
hoại, họ có thể lợi dụng lỗi này để phá vỡ an ninh hệ thống.

1. Tràn bộ nhớ đệm trên stack

10
Bên cạch việc sửa đổi các biến không liên quan, hiện tượng tràn bộ đệm còn
thường bị lợi dụng (khai thác) bởi tin tặc để làm cho một chương trình đang chạy
thực thi một đoạn mã tùy ý được cung cấp. Các kỹ thuật để một tin tặc chiếm
quyền điều khiển một tiến trình tùy theo vùng bộ nhớ mà bộ đệm được đặt tại đó.
Ví dụ, vùng bộ nhớ stack, như đã tìm hiểu ở trên thì đây là nơi dữ liệu có thể được
tạm thời "đẩy" xuống "đỉnh" ngăn xếp (push), và sau đó được "nhấc ra" (pop) để
đọc giá trị của biến. Thông thường, khi một hàm (function) bắt đầu thực thi, các
phần tử dữ liệu tạm thời (các biến địa phương) được đẩy vào, và chương trình có
thể truy nhập đến các dữ liệu này trong suốt thời gian chạy hàm đó. Không chỉ có
hiện tượng tràn stack (stack overflow) mà còn có cả tràn heap (heap overflow).

Ta xem xét 1 ví dụ tổng quát:

"X" là dữ liệu đã từng nằm tại stack khi chương trình bắt đầu thực thi; sau
đó chương trình gọi hàm "Y", hàm này đòi hỏi một lượng nhỏ bộ nhớ cho riêng
mình; và sau đó "Y" gọi hàm "Z", "Z" đòi hỏi một bộ nhớ đệm lớn.

Nếu hàm "Z" gây tràn bộ nhớ đệm, nó có thể ghi đè dữ liệu thuộc về hàm Y
hay chương trình chính.

Điều này đặc biệt nghiêm trọng đối với hầu hết các hệ thống. Ngoài các dữ
liệu thường, bộ nhớ stack còn lưu giữ địa chỉ trả về, nghĩa là vị trí của phần chương
trình đang chạy trước khi hàm hiện tại được gọi. Khi hàm kết thúc, vùng bộ nhớ
tạm thời sẽ được lấy ra khỏi stack, và thực thi được trao lại cho địa chỉ trả về. Như
vậy, nếu địa chỉ trả về đã bị ghi đè bởi một lỗi tràn bộ đệm, nó sẽ trỏ tới một vị trí
nào đó khác. Trong trường hợp một hiện tượng tràn bộ đệm không có chủ ý như
trong ví dụ đầu tiên, hầu như chắc chắn rằng vị trí đó sẽ là một vị trí không hợp lệ,
không chứa một lệnh nào của chương trình, và tiến trình sẽ đổ vỡ. Tuy nhiên, một
kẻ tấn công có thể chỉnh địa chỉ trả về để trỏ tới một vị trí tùy ý sao cho nó có thể
làm tổn hại an hinh hệ thống.

Hãy quan sát 1 ví dụ cụ thể:

1 chương trình viết bằng ngôn ngữ C


int main()
{
char buf[20];
gets(buf);
}

Thực thi chương trình trên trong Linux.

11
gets(buf) sẽ nhận input data vào buf. Kích thước của buf chỉ là 20 bytes.
Nếu ta đẩy data có kích thước lớn hơn 20 bytes vào buf, 20 bytes data đầu tiên sẽ
vào mảng buf[20], các bytes data sau sẽ ghi đè lên EBP cũ và tiếp theo là ret addr.
Như vậy chúng ta có thể thay đổi được địa chỉ trở về, điều này đồng nghĩa với việc
chương trình bị tràn bộ đệm.
đỉnh của bộ nhớ +-------------+ đáy của stack
| return addr |
+-------------+
| EBP cũ |
+-------------+
| |
| |
| buf[20] |
| |
| |
đáy của bộ nhớ +-------------+ đỉnh của stack

Bạn có thể kiểm tra bằng các lệnh trong linux, dễ dàng quan sát được địa chỉ
trở về bị thay đổi, chương trình sẽ thi hành các lệnh tại vị trí thay đổi. Nếu đây là
vùng cấm nên Linux đã báo lỗi "Segmentation fault".

2. Ngôn ngữ lập trình và tràn bộ nhớ đệm

Lựa chọn về ngôn ngữ lập trình có thể có một ảnh hưởng lớn đối với sự xuất
hiện của lỗi tràn bộ đệm. Năm 2006, C và C++ nằm trong số các ngôn ngữ lập
trình thông dụng nhất, với một lượng khổng lồ các phần mềm đã được viết bằng
hai ngôn ngữ này. C và C++ không cung cấp sẵn các cơ chế chống lại việc truy
nhập hoặc ghi đè dữ liệu lên bất cứ phần nào của bộ nhớ thông qua các con trỏ bất
hợp lệ; cụ thể, hai ngôn ngữ này không kiểm tra xem dữ liệu được ghi vào một
mảng (cài đặt của một bộ nhớ đệm) có nằm trong biên của mảng đó hay không.
Tuy nhiên, cần lưu ý rằng các thư viện chuẩn của C++, thư viện khuôn mẫu chuẩn
- STL, cung cấp nhiều cách an toàn để lưu trữ dữ liệu trong bộ đệm, và các lập
trình viên C cũng có thể tạo và sử dụng các tiện ích tương tự. Cũng như đối với các
tính năng bất kỳ khác của C hay C++, mỗi lập trình viên phải tự xác định lựa chọn
xem họ có muốn chấp nhận các hạn chế về tốc độ chương trình để thu lại các lợi
ích tiềm năng (độ an toàn của chương trình) hay không.

Một số biến thể của C, chẳng hạn Cyclone, giúp ngăn chặn hơn nữa các lỗi
tràn bộ đệm bằng việc chẳng hạn như gắn thông tin về kích thước mảng với các
mảng. Ngôn ngữ lập trình D sử dụng nhiều kỹ thuật đa dạng để tránh gần hết việc
sử dụng con trỏ và kiểm tra biên do người dùng xác định.

12
Nhiều ngôn ngữ lập trình khác cung cấp việc kiểm tra tại thời gian chạy,
việc kiểm tra này gửi một cảnh báo hoặc ngoại lệ khi C hoặc C++ ghi đè dữ liệu.
Ví dụ về các ngôn ngữ này rất đa dạng, từ Python tới Ada, từ Lisp tới Modula-2, và
từ Smalltalk tới OCaml. Các môi trường bytecode của Java và .NET cũng đòi hỏi
kiểm tra biên đối với tất cả các mảng. Gần như tất cả các ngôn ngữ thông dịch sẽ
bảo vệ chương trình trước các hiện tượng tràn bộ đệm bằng cách thông báo một
trạng thái lỗi định rõ (well-defined error). Thông thường, khi một ngôn ngữ cung
cấp đủ thông tin về kiểu để thực hiện kiểm tra biên, ngôn ngữ đó thường cho phép
lựa chọn kích hoạt hay tắt chế độ đó. Việc phân tích tĩnh (static analysis) có thể
loại được nhiều kiểm tra kiểu và biên động, nhưng các cài đặt tồi và các trường
hợp rối rắm có thể giảm đáng kể hiệu năng. Các kỹ sư phần mềm phải cẩn thận cân
nhắc giữa các phí tổn cho an toàn và hiệu năng khi quyết định sẽ sử dụng ngôn ngữ
nào và cấu hình như thế nào cho trình biên dịch.

3. Chống tràn bộ nhớ đệm trên stack

Stack-smashing protection là kỹ thuật được dùng để phát hiện các hiện


tượng tràn bộ đệm phổ biến nhất. Kỹ thuật này kiểm tra xem stack đã bị sửa đổi
hay chưa khi một hàm trả về. Nếu stack đã bị sửa đổi, chương trình kết thúc bằng
một lỗi segmentation fault. Các hệ thống sử dụng kỹ thuật này gồm có Libsafe,
StackGuard và các bản vá lỗi (patch) ProPolice gcc.

Chế độ Data Execution Prevention (cấm thực thi dữ liệu) của Microsoft bảo
vệ thẳng các con trỏ tới SEH Exception Handler, không cho chúng bị ghi đè.

Có thể bảo vệ stack hơn nữa bằng cách phân tách stack thành hai phần, một
phần dành cho dữ liệu và một phần cho các bước trả về của hàm. Sự phân chia này
được dùng trong ngôn ngữ lập trình Forth, tuy nó không phải một quyết định thiết
kế dựa theo tiêu chí an toàn. Nhưng dù sao thì đây cũng không phải một giải pháp
hoàn chỉnh đối với vấn đề tràn bộ đệm, khi các dữ liệu nhạy cảm không phải địa
chỉ trả về vẫn có thể bị ghi đè.

13
Kết Luận:
Vấn đề bộ nhớ đệm, stack hệ thống là những vấn đề liên quan đến hệ điều hành và
có vai trò vô cùng quan trọng, đặc biệt là đối với các lập trình viên. Qua việc tìm
hiểu đề tài này, chúng em đã biết thêm kiến thức về những vấn đề trên, và rút ra
được nhiều kinh nghiệm quý trong việc lập trình an toàn, cũng như an ninh hệ
thống. Chúng em xin cảm ơn thầy Đỗ Văn Uy đã hướng dẫn nhóm thực hiện đề tài.
Tuy nhiên, do còn hạn chế về mặt tài liệu và thời gian tìm hiểu không nhiều, chắc
chắn đề tài sẽ còn nhiều thiếu xót, mong được thầy giáo tất cả mọi người đóng góp
ý kiến.

14

You might also like