Sau khi đọc bài: https://daohainam.com/2021/08/13/cau-chuyen-ve-con-tro-pointer, bạn đã biết biến con trỏ là một số nguyên chứa địa chỉ của một vùng nhớ. Tuy nhiên đôi khi bạn còn nghe về khái niệm con trỏ gần và con trỏ xa (near pointer và far pointer), vậy chúng là gì?
Với các CPU đời cũ thời 16bit (đọc tiếp: 64 bit? 32 bit?), để định vị được đến tất cả các địa chỉ trong bộ nhớ, CPU dùng cơ chế segment:offset. Như đã đọc trong bài con trỏ, nếu thanh ghi địa chỉ có kích thước 16 bit, nó chỉ có thể chứa một địa chỉ từ 0-65535 (64KB), một biến con trỏ khi đó cũng chỉ chứa được một địa chỉ trong phạm vi tương tự.
Tuy nhiên, các CPU này lại có tới 20 đường địa chỉ, tức là giữa CPU và MCU (Memory Control Unit) có tới 20 “sợi dây điện” (gọi vậy cho dễ hình dung :D, đặt tên là A0-A19), do vậy CPU có thể gửi 1 con số lớn tới 20 bit đến MCU mỗi khi nó cần đọc/ghi 1 giá trị trong bộ nhớ. Vì một thanh ghi chỉ có kích thước 16 bit, do vậy nếu muốn lưu lại một địa chỉ, CPU phải kết hợp 2 thanh ghi lại với nhau. Người ta chia bộ nhớ thành từng phân đoạn (segment), mỗi segment sẽ bắt đầu tại một địa chỉ cách nhau 16 byte (1). Như vậy, segment 0 bắt đầu từ địa chỉ 0, segment 1 bắt đầu từ địa chỉ 16, segment 2 bắt đầu từ 32… và cứ như vậy.
Mỗi đoạn như vậy dài 64KB (65536), vì chỉ bắt đầu cách nhau 16 byte, nhưng lại dài tới 65536 byte, nên các đoạn sẽ trùng lên nhau, ví dụ byte thứ 17 trong segment 0 chính là byte đầu tiên (byte 0) của segment 1. Địa chỉ tương đối trong một segment được gọi là địa chỉ offset. Một sự kết hợp giữa segment và offset sẽ cho phép ta truy cập đến vị trí bất kỳ trong dải địa chỉ 1MB (2^20) đã nói ở trên.
Những địa chỉ sau sẽ cùng chỉ vào một vị trí trong bộ nhớ: 0:32 == 1:16 == 2:0
Tới đây, bạn sẽ đặt câu hỏi: Tại sao không nối 2 địa chỉ 16bit lại ra luôn thành 32bit, vậy có phải sau này khi nghiên cứu ra bộ nhớ lớn hơn, người ta không cần phải thay đổi gì, như vậy không đơn giản hơn sao?
Thứ nhất, 1MB thời đó là con số rất lớn, nếu bạn biết rằng máy điện tử 4 nút NES, ra đời sau CPU 8086 5 năm, có thể gọi là PlayStation của thời đó, chỉ có 2KB RAM, bạn hẳn sẽ không ngạc nhiên về con số 1MB “lớn kinh khủng” này.
Thứ hai, quan trọng hơn, đó là nhờ việc chia ra thành từng segment, sau đó truy cập vào các địa chỉ trong segment đó thông qua offset, bạn có thể đơn giản hóa rất nhiều khi quản lý các chương trình. Hãy tưởng tượng chương trình của bạn có 1 biến static tên là x, khi dịch ra nó nằm ở byte thứ 100 trong chương trình, vì mỗi lần chương trình được tải vào bộ nhớ lại nằm ở một vị trí khác nhau nên địa chỉ của x cũng sẽ thay đổi. Nếu chương trình được tải vào địa chỉ 1000, vậy muốn gán giá trị 0 cho x, ta phải viết [1000 + 100] = 0. Bản thân số 1000 này cũng phải được lưu vào một biến khác nữa, vì mỗi lần tải vào bộ nhớ lại là một giá trị mới, nên sẽ gây ra rất nhiều sự rối rắm phức tạp.
Nhờ cơ chế phân bộ nhớ thành từng đoạn cách nhau 16 byte này, hệ điều hành chỉ đơn giản tải chương trình của bạn vào vị trị bắt đầu một segment (có địa chỉ tuyệt đối là bội số của 16), đặt địa chỉ segment mặc nhiên vào một thanh ghi segment, sau đó chỉ cần truy cập vào biến x thông qua địa chỉ tương đối của nó (tức là địa chỉ offset), trong ví dụ này là [100], bạn không cần quan tâm đến việc phân đoạn này nằm ở đâu nữa mà chỉ cần biết địa chỉ tương đối so với phân đoạn đó.
Trong C/C++, mặc nhiên khi khai báo một biến con trỏ, biến đó sẽ chỉ chứa địa chỉ offset, ta gọi nó là con trỏ gần. Nếu một biến được khai báo với từ khóa far, nó sẽ chứa cả địa chỉ segment, ta gọi nó là con trỏ xa.
Con trỏ gần chỉ có thể tham chiếu đến một giá trị trong cùng phân đoạn, ngược lại, con trỏ xa có thể truy cập vào bất kỳ vị trí nào trong bộ nhớ.
int
* near_ptr; int
far * far_ptr;
Trong ví dụ trên, near_ptr có kích thước 2 byte, far_ptr có kích thước 4 byte.
Trong các thế hệ CPU sau này, cơ chế đánh địa chỉ đã thay đổi và không còn dùng cơ chế segment:offset, do vậy không còn khái niệm con trỏ gần và con trỏ xa nữa. Nói một cách đơn giản là với thế hệ 32 bit thì một “segment” đã có kích thước tới 4GB, thế hệ 64 bit thì một “segment” còn to hơn rất rất nhiều, chưa kể việc định vị các “segment” đã phức tạp hơn là thông qua một cơ chế tuyến tính với mỗi 16 byte cách nhau. Nếu bạn dùng từ khóa far với các trình biên dịch mới, rất có thể nó sẽ báo lỗi.
(1) nói cách khác, để định vị được 1MB(2^20), ta cần 20 bit địa chỉ, do đó ta kết hợp 4 bit từ segment và 16 bit từ offset.