Để hiểu được một chương trình C chạy như thế nào trên hệ điều hành Unix (ví dụ như Linux hoặc MacOS), chúng ta sẽ đi qua 3 chủ đề:
- Cách compile một source code C thành file binary (có thể chạy trực tiếp được từ hệ hiều hành - giống file .exe trên Windows)
- Cách sắp xếp các phần cứng cơ bản của 1 máy tính
- Thực hiện việc chạy file binary
Source code sẽ là một chương trình "hello, world" đơn giản viết bằng C:
#include <stdio.h>
int main() { printf("hello, world"); return 0;
}
Tạo file thực thi binary
Như ta thấy trên biểu đồ, source code hello
khi được compile bởi GCC
compiler sẽ trải qua 4 giai đoạn trước khi trở thành file thực thi binary:
- Preprocessor phase: Thêm các header, xoá comments...
- Compiler phase: Kiểm tra lỗi chính tả, chuyển đổi ngôn ngữ C thành ngôn ngữ assembly
- Assembler phase: Chuyển đổi assemby code thành binary, file này được gọi là "relocatable object program". Chương trình lúc này có thể được kết nối với code của các chương trình khác để chạy, ví dụ như chương trình
printf
. - Linker phase: Khác với Preprocessor Phase là chỉ thêm header (type) để kiểm tra lỗi chính tả, ở phase này sẽ thêm trực tiếp code của các thư viện và chương trình khác (implementation) để tạo thành 1 file thực thi binary duy nhất.
Lúc này chúng ta sẽ có 1 file binary duy nhất là
hello
, file này có thể được chạy trực tiếp từ chương trìnhshell
(bash
) của hệ điều hành như sau:
./hello
Bố cục phần cứng
Chú ý: Mình sẽ giữ các từ khoá tiếng anh để các bạn tiện tra cứu trên google
Trước khi chạy chương trình hello
, chúng ta sẽ đi sơ qua về cách phần cứng được sắp xếp trong một máy tính cơ bản.
Buses
Là tập hợp các ống dây dẫn điện để truyền thông tin dưới dạng bytes
giữa các phần cứng. Bytes
được vận chuyển theo đơn vị là word
, chứa 4 bytes (hệ điều hành 32-bit) hoặc 8 bytes (hệ điều hành 64-bit)
Dữ liệu được vận chuyển giữa các phần cứng thông qua buses
. Có một trạm trung tâm là I/O bridge
có trách nhiệm:
- Vận chuyển dữ liệu đến và đi giữa
CPU
thông quaSystem Bus
- Vận chuyển dữ liệu đến và đi giữa
Main Memory
thông quaMemory Bus
. - Vận chuyển dữ liệu đến và đi giữa
I/O devices
thông quaI/O Bus
.
I/O devices
Có 4 thiết bị I/O devices chính:
- bàn phím với nhiệm vụ đầu vào
- chuột với nhiệm vụ đầu vào
- màn hình với nhiệm vụ đầu ra
- ổ đỉa để lưu trữ dài hạn các dữ liệu và chương trình
Dữ liệu sẽ được chuyển từ
I/O devices
đến các phần cứng khác nhưMain Memory
,CPU
thông quaBuses
Main Memory
Main Memory
là thiết bị lưu trữ tạm thời (tắt điện là sẽ bị xoá) lưu giữ code chạy chương trình và dữ liệu. Các dữ liệu này sẽ được Processor
sử dụng và thay đổi trong quá trình chạy chương trình.
Về mặt vật lý, Main Memory
chứa các chips
bộ nhớ truy cập ngẫu nhiên động (dynamic random access memory - DRAM).
Về mặt logic, Main Memory
chưa các mảng bytes
liên tiếp, mỗi byte sẽ có 1 địa chỉ duy nhất. Chúc ta có thể truy xuất và cập nhật nhanh mỗi byte dựa vào các địa chỉ này.
Processor (hay còn gọi là central processing unit - CPU)
Trong CPU
, chúng ta có:
Program Counter
(là mộtregister
đặc biệt): Lưu giữ các con trỏ tới các địa chỉ của code chương trình trongMain Memory
. Program Counter sẽ đảm bảo việc thực thi tuần tự các dòng code.Register File
: Chứa cácregisters
thông thường, kích cỡ củaregister
là 1word
. Mỗiregister
sẽ lưu giữ dữ liệu được CPU tải về từMain Memory
hoặc dữ liệu sau khi đã tính toán từALU
.Arithmetic/Logic Unit (ALU)
: Tính toán các dữ liệu đầu vào từregisters
và trả về kết quả lưu lại vàoregisters
.
Chạy chương trình
Chúng ta đã có file thực thi binary và góc nhìn tổng quan về cách sắp xếp các phần cứng cơ bản. Đã đến lúc chạy chương trình hello
.
Để chạy chương trình, chúng ta gõ tên chương trình vào shell
. Sau khi chạy ta sẽ thấy dòng chữ hello, world
hiển thị trên màn hình.
./hello
hello, world
Đây là các bước mà chương trình sẽ được chạy:
- Ban đầu, trước khi chạy chương trình,
shell
sẽ chờ đợi người dùng gõ tên chương trình. - Người dùng gõ "./hello" và bấm enter, thông báo với hệ thống là hãy chạy file thực thi
hello
- Phần code binary của file
hello
sẽ được copy từDisk
lênMain Memory
- Trong chương trình, chúng ta thấy chỉ có 1 dòng hướng dẫn ở trong file
hello
, (printf("hello, world")
). CPU sẽ lưu lại 1 con trỏ trênProgram Counter
, trỏ đến địa chỉ của dòng lệnh này trênMain Memory
- Khác với dòng lệnh chỉ được copy địa chỉ, với data thì sẽ copy trực tiếp. Data (
hello, world
) được copy từMain Memory
vàoRegister
trongRegister File
- Trong chương trình
hello
, chúng ta không có dòng lệnh nào về tính toán (a +b) hoặc bitwise (a ==b), vì vậy chúng ta không cần sự tham gia củaALU
. CPU sẽ copy dữ liệu chuỗihello, world
từRegister
và vận chuyển đếnDisplay
. - Người dùng sẽ thấy chữ
hello, world
được in lên màn hình - Chương trình kết thúc,
shell
chờ đợi lệnh tiếp theo từ người dùng
Đó là cách một chương trình hello
đơn giản chạy trên hệ điều hành Unix (tiền thân của Linux và MacOS)
Cảm ơn các bạn đã chịu khó đọc đến đây.
Hình ảnh và nội dung được tham khảo từ cuốn sách Computer Systems: A Programmer's Perspective
Bài viết gốc trên blog của mình: https://www.anvo.dev/articles/how-does-a-c-program-run-on-a-unix-system