Khi máy tính lưu trữ một biến, khi một chương trình cần lấy giá trị của biến đó, làm thế nào để máy tính biết vị trí tìm trong bộ nhớ cho giá trị của biến đó?
Khi máy tính lưu trữ một biến, khi một chương trình cần lấy giá trị của biến đó, làm thế nào để máy tính biết vị trí tìm trong bộ nhớ cho giá trị của biến đó?
Câu trả lời:
Tôi khuyên bạn nên nhìn vào thế giới tuyệt vời của Compiler Construction! Câu trả lời là đó là một chút của một quá trình phức tạp.
Để cố gắng cung cấp cho bạn một trực giác, hãy nhớ rằng các tên biến hoàn toàn là vì lợi ích của lập trình viên. Máy tính cuối cùng sẽ biến mọi thứ thành địa chỉ ở cuối.
Các biến cục bộ (thường) được lưu trữ trên ngăn xếp: nghĩa là chúng là một phần của cấu trúc dữ liệu đại diện cho một lệnh gọi hàm. Chúng ta có thể xác định danh sách đầy đủ các biến mà một hàm sẽ (có thể) sử dụng bằng cách xem hàm đó, vì vậy trình biên dịch có thể thấy nó cần bao nhiêu biến cho hàm này và mỗi biến mất bao nhiêu dung lượng.
Có một chút phép thuật gọi là con trỏ ngăn xếp, là một thanh ghi luôn lưu địa chỉ nơi ngăn xếp hiện tại bắt đầu.
Mỗi biến được đưa ra một "offset offset", đó là nơi lưu trữ trong ngăn xếp. Sau đó, khi chương trình cần phải truy cập vào một biến x
, này thay thế biên dịch x
với STACK_POINTER + x_offset
, để có được những vị trí vật lý thực tế nó được lưu trữ trong bộ nhớ.
Lưu ý rằng, đây là lý do tại sao bạn lấy lại một con trỏ khi bạn sử dụng malloc
hoặc new
trong C hoặc C ++. Bạn không thể xác định chính xác vị trí của bộ nhớ được phân bổ heap ở đâu trong bộ nhớ, vì vậy bạn phải giữ một con trỏ tới nó. Con trỏ đó sẽ nằm trên stack, nhưng nó sẽ trỏ đến heap.
Chi tiết về việc cập nhật ngăn xếp cho các cuộc gọi và trả về chức năng rất phức tạp, vì vậy tôi muốn giới thiệu Sách Rồng hoặc Sách Hổ nếu bạn quan tâm.
Khi máy tính lưu trữ một biến, khi một chương trình cần lấy giá trị của biến đó, làm thế nào để máy tính biết vị trí tìm trong bộ nhớ cho giá trị của biến đó?
Chương trình nói với nó. Máy tính thực chất không có khái niệm về "biến" - đó hoàn toàn là một thứ ngôn ngữ cấp cao!
Đây là chương trình C:
int main(void)
{
int a = 1;
return a + 3;
}
và đây là mã lắp ráp mà nó biên dịch thành: (bình luận bắt đầu bằng ;
)
main:
; {
pushq %rbp
movq %rsp, %rbp
; int a = 1
movl $1, -4(%rbp)
; return a + 3
movl -4(%rbp), %eax
addl $3, %eax
; }
popq %rbp
ret
Với "int a = 1;" CPU thấy lệnh "lưu trữ giá trị 1 tại địa chỉ (giá trị của thanh ghi rbp, trừ 4)". Nó biết nơi lưu trữ giá trị 1 vì chương trình cho nó biết.
Tương tự, lệnh tiếp theo nói "tải giá trị tại địa chỉ (giá trị của thanh ghi rbp, trừ 4) vào thanh ghi eax". Máy tính không cần biết về những thứ như biến.
%rsp
là con trỏ ngăn xếp của CPU. %rbp
là một thanh ghi đề cập đến bit của ngăn xếp được sử dụng bởi hàm hiện tại. Sử dụng hai thanh ghi đơn giản hóa việc gỡ lỗi.
Khi trình biên dịch hoặc trình thông dịch gặp phải khai báo một biến, nó sẽ quyết định địa chỉ nào nó sẽ sử dụng để lưu trữ biến đó và sau đó ghi lại địa chỉ trong bảng ký hiệu. Khi gặp các tham chiếu tiếp theo đến biến đó, địa chỉ từ bảng ký hiệu được thay thế.
Địa chỉ được ghi trong bảng ký hiệu có thể là phần bù từ một thanh ghi (chẳng hạn như con trỏ ngăn xếp) nhưng đó là một chi tiết thực hiện.
Các phương pháp chính xác phụ thuộc vào những gì bạn đang nói cụ thể và mức độ bạn muốn đi sâu. Ví dụ, lưu trữ tệp trên ổ cứng khác với lưu trữ thứ gì đó trong bộ nhớ hoặc lưu trữ thứ gì đó trong cơ sở dữ liệu. Mặc dù các khái niệm là tương tự nhau. Và cách bạn thực hiện nó ở cấp độ lập trình là một cách giải thích khác với cách máy tính thực hiện nó ở cấp I / O.
Hầu hết các hệ thống sử dụng một số loại cơ chế thư mục / chỉ mục / đăng ký để cho phép máy tính tìm và truy cập dữ liệu. Chỉ mục / thư mục này sẽ chứa một hoặc nhiều khóa và địa chỉ dữ liệu thực sự nằm trong đó (cho dù đó là ổ cứng, RAM, cơ sở dữ liệu, v.v.).
Ví dụ chương trình máy tính
Một chương trình máy tính có thể truy cập bộ nhớ theo nhiều cách khác nhau. Thông thường, hệ điều hành cung cấp cho chương trình một không gian địa chỉ và chương trình có thể thực hiện những gì nó muốn với không gian địa chỉ đó. Nó có thể ghi trực tiếp vào bất kỳ địa chỉ nào trong không gian bộ nhớ của nó và nó có thể theo dõi cách nó muốn. Điều này đôi khi sẽ thay đổi theo ngôn ngữ lập trình và hệ điều hành, hoặc thậm chí theo các kỹ thuật ưa thích của lập trình viên.
Như đã đề cập trong một số câu trả lời khác, mã hóa hoặc lập trình chính xác được sử dụng khác nhau, nhưng thông thường đằng sau hậu trường, nó sử dụng một cái gì đó giống như một ngăn xếp. Nó có một thanh ghi lưu trữ vị trí bộ nhớ nơi ngăn xếp hiện tại bắt đầu, và sau đó một phương thức để biết vị trí trong ngăn xếp đó là một hàm hoặc biến.
Trong nhiều ngôn ngữ lập trình cấp cao hơn, nó sẽ chăm sóc tất cả những thứ đó cho bạn. Tất cả những gì bạn phải làm là khai báo một biến và lưu trữ thứ gì đó trong biến đó và nó tạo ra các ngăn xếp và mảng cần thiết phía sau hậu trường cho bạn.
Nhưng xem xét cách lập trình linh hoạt, thực sự không có một câu trả lời, vì lập trình viên có thể chọn viết trực tiếp vào bất kỳ địa chỉ nào trong không gian được phân bổ bất cứ lúc nào (giả sử anh ta đang sử dụng ngôn ngữ lập trình cho phép điều đó). Sau đó, anh ta có thể lưu trữ vị trí của nó trong một mảng hoặc thậm chí chỉ mã cứng nó vào chương trình (tức là biến "alpha" luôn được lưu trữ ở đầu ngăn xếp hoặc luôn được lưu trữ trong 32 bit đầu tiên của bộ nhớ được phân bổ).
Tóm lược
Về cơ bản, phải có một số cơ chế đằng sau hậu trường cho máy tính biết nơi lưu trữ dữ liệu. Một trong những cách phổ biến nhất là một số loại chỉ mục / thư mục chứa khóa (s) và địa chỉ bộ nhớ. Điều này được thực hiện theo tất cả các cách và thường được gói gọn từ người dùng (và đôi khi thậm chí được gói gọn từ người lập trình).
Tham khảo: Làm thế nào để máy tính nhớ nơi chúng lưu trữ mọi thứ?
Nó biết vì các mẫu và định dạng.
Chương trình / chức năng / máy tính không thực sự biết mọi thứ ở đâu. Nó chỉ mong đợi một cái gì đó được ở một nơi nhất định. Hãy sử dụng một ví dụ.
class simpleClass{
public:
int varA=58;
int varB=73;
simpleClass* nextObject=NULL;
};
Lớp mới 'SimpleClass' của chúng tôi chứa 3 biến quan trọng - hai số nguyên có thể chứa một số dữ liệu khi chúng tôi cần và một con trỏ đến một 'đối tượng SimpleClass' khác. Giả sử rằng chúng tôi đang sử dụng máy 32 bit vì mục đích đơn giản. Trình biên dịch 'gcc' hoặc 'C' khác sẽ tạo mẫu để chúng tôi làm việc để phân bổ một số dữ liệu.
Các loại đơn giản
Đầu tiên, khi một người sử dụng một từ khóa cho một loại đơn giản như 'int', một trình ghi chú được tạo bởi trình biên dịch trong phần '.data' hoặc '.bss' của tệp thực thi để khi được hệ điều hành thực thi, dữ liệu sẽ được thực thi có sẵn cho chương trình. Từ khóa 'int' sẽ phân bổ 4 byte (32 bit), trong khi một 'int dài' sẽ phân bổ 8 byte (64 bit).
Đôi khi, theo cách thức của từng tế bào, một biến có thể xuất hiện ngay sau khi lệnh được cho là tải nó vào bộ nhớ, do đó, nó sẽ trông giống như thế này trong lắp ráp giả:
...
clear register EAX
clear register EBX
load the immediate (next) value into EAX
5
copy the value in register EAX to register EBX
...
Điều này sẽ kết thúc với giá trị '5' trong EAX cũng như EBX.
Trong khi chương trình thực thi, mọi lệnh được thực thi ngoại trừ '5' vì tải ngay lập tức tham chiếu đến nó và làm cho CPU bỏ qua nó.
Nhược điểm của phương pháp này là nó chỉ thực sự thiết thực cho các hằng số, vì sẽ không thực tế khi giữ các mảng / bộ đệm / chuỗi ở giữa mã của bạn. Vì vậy, nói chung, hầu hết các biến được giữ trong các tiêu đề chương trình.
Nếu một người cần truy cập vào một trong các biến động này, thì người ta có thể coi giá trị ngay lập tức như thể nó là một con trỏ:
...
clear register EAX
clear register EBX
load the immediate value into EAX
0x0AF2CE66 (Let's say this is the address of a cell containing '5')
load the value pointed to by EAX into EBX
...
Điều này sẽ kết thúc với giá trị '0x0AF2CE66' trong đăng ký EAX và giá trị '5' trong đăng ký EBX. Người ta cũng có thể thêm các giá trị trong các thanh ghi với nhau, vì vậy chúng ta có thể tìm thấy các phần tử của một mảng hoặc chuỗi bằng phương thức này.
Một điểm quan trọng khác là người ta có thể lưu trữ các giá trị khi sử dụng địa chỉ theo cách tương tự, để người ta có thể tham chiếu các giá trị tại các ô đó sau.
Các loại phức tạp
Nếu chúng ta tạo hai đối tượng của lớp này:
simpleClass newObjA;
simpleClass newObjB;
sau đó chúng ta có thể gán một con trỏ cho đối tượng thứ hai vào trường có sẵn cho nó trong đối tượng thứ nhất:
newObjA.nextObject=&newObjB;
Bây giờ chương trình có thể mong đợi tìm địa chỉ của đối tượng thứ hai trong trường con trỏ của đối tượng thứ nhất. Trong bộ nhớ, nó sẽ trông giống như:
newObjA: 58
73
&newObjB
...
newObjB: 58
73
NULL
Một thực tế rất quan trọng cần lưu ý ở đây là 'newObjA' và 'newObjB' không có tên khi chúng được biên dịch. Chúng chỉ là nơi chúng ta mong đợi một số dữ liệu sẽ xuất hiện. Vì vậy, nếu chúng ta thêm 2 ô vào & newObjA thì chúng ta sẽ tìm thấy ô hoạt động như 'nextObject'. Do đó, nếu chúng ta biết địa chỉ của 'newObjA' và nơi ô 'nextObject' có liên quan đến nó, thì chúng ta có thể biết địa chỉ của 'newObjB':
...
load the immediate value into EAX
&newObjA
add the immediate value to EAX
2
load the value in EAX into EBX
Điều này sẽ kết thúc với '2 + & newObjA' trong 'EAX' và '& newObjB' trong 'EBX'.
Mẫu / Định dạng
Khi trình biên dịch biên dịch định nghĩa lớp, nó thực sự biên dịch một cách để tạo định dạng, cách viết thành định dạng và cách đọc từ định dạng.
Ví dụ được đưa ra ở trên là một mẫu cho danh sách liên kết đơn có hai biến 'int'. Các loại công trình này rất quan trọng đối với việc cấp phát bộ nhớ động, cùng với các cây nhị phân và n-ary. Các ứng dụng thực tế của cây n-ary sẽ là các hệ thống tệp bao gồm các thư mục trỏ đến tệp, thư mục hoặc các phiên bản khác được trình điều khiển / hệ điều hành nhận ra.
Để truy cập tất cả các yếu tố, hãy nghĩ về một con giun inch đang di chuyển lên và xuống cấu trúc. Bằng cách này, chương trình / chức năng / máy tính không biết gì cả, nó chỉ thực hiện các hướng dẫn để di chuyển dữ liệu xung quanh.