Chào mừng bạn đến với chuỗi bài viết tự học lập trình ngôn ngữ C từ số 0. C là một trong những ngôn ngữ lập trình nền tảng, giúp bạn hiểu sâu sắc cách máy tính quản lý bộ nhớ, tối ưu hóa phần cứng và xây dựng tư duy lập trình vững chắc. Để bắt đầu viết những dòng code C đầu tiên, việc thiết lập một môi trường làm việc chuẩn mực và trực quan là vô cùng cần thiết. Trong bài viết đầu tiên này, chúng ta sẽ cùng thiết lập bộ biên dịch (Compiler) và trình soạn thảo (Editor) trên macOS, Linux và Windows.
1. Quy trình biên dịch mã nguồn C hoạt động thế nào?
Khác với các ngôn ngữ thông dịch như JavaScript hay Python (chạy trực tiếp thông qua engine thông
dịch), C là một ngôn ngữ biên dịch. Khi bạn viết mã nguồn C (lưu dưới dạng file .c), máy
tính không thể hiểu trực tiếp các dòng chữ tiếng Anh đó. Trình biên dịch (Compiler) sẽ chịu trách
nhiệm chuyển dịch văn bản của bạn qua nhiều giai đoạn trung gian để tạo ra file thực thi nhị phân mà
CPU hiểu được.
Việc chuyển dịch đó không xảy ra trong một bước. Nó đi qua bốn giai đoạn nối tiếp nhau, mỗi giai đoạn nhận đầu ra của giai đoạn trước và tạo ra một file trung gian mới:
[hello.c (Source Code)]
│
▼ (Giai đoạn 1: Preprocessor - Tiền xử lý)
[hello.i (Preprocessed Code)]
│
▼ (Giai đoạn 2: Compiler - Biên dịch sang mã Assembly)
[hello.s (Assembly Code)]
│
▼ (Giai đoạn 3: Assembler - Lắp ráp sang mã máy thô)
[hello.o (Object File)] ─────────┐
▼ (Giai đoạn 4: Linker - Liên kết thư viện)
[hello (Executable Binary)]
Đọc sơ đồ trên từ trên xuống, bốn giai đoạn đó làm những việc sau:
-
Tiền xử lý (Preprocessing) — xử lý các dòng bắt đầu bằng dấu
#. Hiểu đơn giản: nó là bước "dán chồng", chép nguyên nội dung các file khác vào file của bạn trước khi dịch. Kết quả vẫn là văn bản C, chỉ dài hơn rất nhiều. - Biên dịch (Compilation) — dịch văn bản C sang Assembly (hợp ngữ): một ngôn ngữ vẫn đọc được bằng mắt nhưng đã bám sát cách CPU làm việc, mỗi dòng gần như tương ứng một lệnh của con chip. Đây là bước "dịch sang tiếng của máy", và bản dịch khác nhau tuỳ loại chip.
- Lắp ráp (Assembly) — đổi từng lệnh Assembly thành các con số nhị phân mà CPU thi hành trực tiếp. Kết quả gọi là file đối tượng (object file): đã là mã máy, nhưng còn thiếu, vì trong đó có những chỗ gọi tới hàm nằm ở nơi khác mà nó chưa biết địa chỉ.
-
Liên kết (Linking) — ghép file đối tượng của bạn với mã máy có sẵn của hệ thống,
rồi điền nốt những địa chỉ còn trống. Ví dụ
printfkhông phải bạn viết ra: nó nằm sẵn trong thư viện C chuẩn của hệ điều hành, và bước này là lúc chương trình của bạn được nối vào đó. Xong bước này mới có file chạy được.
hello.c để thao tác. Bốn mục tiếp theo lo
phần chuẩn bị đó.
2. Cài đặt Trình biên dịch (Compiler)
Trên hệ điều hành macOS
Trình biên dịch mặc định trên macOS là Clang (được bọc dưới lệnh gcc để
tương thích). Để cài đặt nhanh gọn nhất, bạn không cần tải toàn bộ bộ Xcode nặng hàng chục GB, chỉ cần
cài đặt gói công cụ dòng lệnh (Command Line Tools):
-
Mở ứng dụng Terminal (nhấn
Cmd + Space, gõ "Terminal" và nhấn Enter). - Nhập lệnh sau và nhấn Enter:
xcode-select --install
Một hộp thoại xác nhận sẽ hiện lên yêu cầu bạn đồng ý cài đặt. Hãy bấm Install và đợi hệ thống tải xuống hoàn tất.
Trên hệ điều hành Linux
Hầu hết các bản phân phối Linux sử dụng bộ trình biên dịch GCC (GNU Compiler Collection) cực kỳ mạnh mẽ. Để cài đặt bộ công cụ phát triển cơ bản (bao gồm GCC, Make và các thư viện hệ thống cần thiết):
Đối với Ubuntu/Debian/Mint, mở Terminal và chạy lệnh:
sudo apt update
sudo apt install build-essential
Đối với Fedora/CentOS/RHEL:
sudo dnf groupinstall "Development Tools"
Trên hệ điều hành Windows
C trên Windows chạy được, nhưng bộ công cụ gốc của Windows khác Unix ở nhiều chỗ nhỏ mà cả series này lại dùng lệnh Unix xuyên suốt. Cách gọn nhất để không phải dịch lại từng lệnh là bật WSL (Windows Subsystem for Linux) — một bản Linux chạy ngay trong Windows, dùng chung file với máy bạn. Mở PowerShell với quyền quản trị và chạy:
wsl --install
Khởi động lại máy, mở ứng dụng Ubuntu vừa được cài, rồi chạy đúng lệnh
apt ở phần Linux phía trên. Từ đó trở đi mọi lệnh trong series này chạy y hệt như trên
Linux. Trong VS Code, cài thêm tiện ích WSL của Microsoft để mở thẳng thư mục bên
trong Ubuntu.
\ thay vì /, không có sẵn make, và
gdb phải cài riêng. Đi đường WSL thì mọi lệnh, mọi đường dẫn và mọi ảnh chụp màn hình
trong series đều khớp với máy bạn.
Kiểm tra cài đặt compiler thành công
Để chắc chắn máy tính của bạn đã có compiler, gõ lệnh kiểm tra phiên bản sau trong Terminal:
gcc --version
Nếu màn hình hiển thị thông tin phiên bản của gcc hoặc Apple clang, xin chúc
mừng, bạn đã sẵn sàng!
3. Cài đặt Trình soạn thảo mã nguồn (VS Code)
Chúng ta sẽ sử dụng Visual Studio Code (VS Code) - một code editor nhẹ, miễn phí, và cực kỳ phổ biến của Microsoft.
- Truy cập trang chủ code.visualstudio.com để tải về file cài đặt phù hợp với macOS hoặc Linux của bạn.
- Cài đặt ứng dụng và mở VS Code lên.
-
Cài đặt Tiện ích mở rộng (Extension) hỗ trợ ngôn ngữ C:
-
Nhấn vào biểu tượng Extensions ở thanh lề trái (hoặc bấm tổ hợp phím
Cmd + Shift + X). - Tìm kiếm từ khóa: C/C++ Extension Pack (của Microsoft).
- Bấm nút Install để cài đặt. Tiện ích này sẽ mang lại khả năng gợi ý mã nguồn (IntelliSense), định dạng code tự động và gỡ lỗi (debugging).
-
Nhấn vào biểu tượng Extensions ở thanh lề trái (hoặc bấm tổ hợp phím
4. Viết và chạy chương trình C đầu tiên
Giờ là lúc viết chương trình huyền thoại: hiển thị dòng chữ "Hello, World!".
- Mở một thư mục trống bằng VS Code (File -> Open Folder).
- Tạo một file mới tên là
hello.c. - Nhập đoạn mã nguồn chuẩn mực sau vào file:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
Sáu dòng này là chương trình C nhỏ nhất còn có ích, và mỗi dòng đều có việc của nó:
-
#include <stdio.h>— dòng bắt đầu bằng#nên nó là chỉ thị cho bộ tiền xử lý ở mục 1: hãy chép nội dung filestdio.hvào đây. Đó là nơi khai báoprintf. Thiếu dòng này thì trình biên dịch không biếtprintflà gì. -
int main() { ... }— hàmmainlà điểm bắt đầu: khi chạy chương trình, hệ điều hành luôn gọi vào đây trước tiên. Mọi chương trình C đều phải có đúng một hàm tênmain. Cặp ngoặc nhọn{ }bao lấy phần thân — những việc cần làm. -
printf("Hello, World!\n");— in một dòng chữ ra màn hình.\nở cuối là ký tự xuống dòng; bỏ nó đi thì dấu nhắc Terminal sẽ dính ngay sau chữWorld!. Dấu chấm phẩy cuối câu là bắt buộc — C dùng nó để biết một lệnh đã hết. -
return 0;— báo cho hệ điều hành rằng chương trình kết thúc bình thường. Theo quy ước của Unix, 0 nghĩa là thành công, khác 0 nghĩa là có lỗi. Chữintđứng trướcmainchính là để nói "hàm này trả về một số nguyên", vàreturn 0;là số nguyên đó.
Lưu file lại (Cmd + S).
Biên dịch và chạy qua Terminal
Mở Terminal tích hợp ngay trong VS Code bằng phím tắt Ctrl + ` (dấu huyền) hoặc chọn
Terminal -> New Terminal trên thanh công cụ.
1. Tiến hành biên dịch file hello.c thành file thực thi tên là hello:
gcc hello.c -o hello
2. Chạy chương trình vừa được biên dịch xong:
./hello
Bạn sẽ thấy dòng chữ Hello, World! hiển thị ngay trên Terminal của mình. Bạn đã chính
thức bước chân vào thế giới lập trình ngôn ngữ C!
📥 Tải về mã nguồn mẫu: hello.c
5. Tự tay đi qua 4 giai đoạn biên dịch
Giờ máy đã có trình biên dịch và thư mục làm việc đã có hello.c — đủ điều kiện để mở từng
giai đoạn đã nói ở mục 1 ra xem tận mắt. Chạy lần lượt các lệnh dưới đây trong Terminal, ngay tại thư
mục chứa hello.c. Mỗi lệnh sinh ra một file mới, và bạn nên mở file đó bằng VS Code sau
mỗi bước.
Giai đoạn 1: Tiền xử lý (Preprocessing)
Bộ tiền xử lý (Preprocessor) quét mã nguồn và thực thi các chỉ thị bắt đầu bằng dấu
# (như #include, #define, #ifdef). Các hằng số
định nghĩa qua macro sẽ được chèn trực tiếp, các tệp tiêu đề (header files) sẽ được sao chép hoàn toàn
nội dung vào file. Bạn có thể xuất kết quả tiền xử lý bằng lệnh:
gcc -E hello.c -o hello.i
Nếu mở file hello.i vừa tạo ra, bạn sẽ thấy nó dài hàng trăm dòng vì chứa toàn bộ mã khai
báo hàm của tệp tiêu đề stdio.h được chèn vào đầu.
Giai đoạn 2: Biên dịch (Compilation)
Trình biên dịch nhận đầu vào là mã nguồn đã được tiền xử lý (hello.i) và phân tích cú
pháp, chuyển đổi các cấu trúc lệnh của ngôn ngữ C sang mã Assembly (hợp ngữ) của cấu trúc chip CPU
tương ứng (như x86_64 hoặc ARM). Để xuất file mã Assembly, hãy chạy lệnh:
gcc -S hello.i -o hello.s
Khi mở file hello.s, bạn sẽ thấy các lệnh hợp ngữ cơ bản như pushq,
movq, call, ret...
Giai đoạn 3: Lắp ráp (Assembly)
Bộ lắp ráp (Assembler) chuyển đổi mã Assembly văn bản (hello.s) thành mã máy nhị phân thô
(Machine Code). Kết quả là một file đối tượng (Object file). File này chứa mã máy của CPU nhưng chưa
thể chạy độc lập vì chưa có địa chỉ liên kết các hàm thư viện ngoài. Xuất file Object bằng lệnh:
gcc -c hello.s -o hello.o
Giai đoạn 4: Liên kết (Linking)
Bộ liên kết (Linker) sẽ ghép nối file đối tượng (hello.o) của bạn với mã nhị phân của các
thư viện hệ thống đã được biên dịch sẵn (như hàm printf nằm trong thư viện C chuẩn
libc.a hoặc libc.so). Linker giải quyết các địa chỉ ô nhớ và gộp tất cả
thành file nhị phân thực thi cuối cùng:
gcc hello.o -o hello
Bốn lệnh vừa rồi làm đúng việc mà một lệnh gcc hello.c -o hello ở mục 4 đã làm gọn trong
một lần. Điều đáng nhớ: khi chương trình lớn lên và báo lỗi, thông báo lỗi sẽ nói rõ nó đến từ giai
đoạn nào — lỗi cú pháp là của giai đoạn 2, còn undefined reference là của giai đoạn 4.
Biết lỗi thuộc giai đoạn nào là đã đi được nửa đường sửa nó.
6. Khắc phục các lỗi biên dịch thường gặp
-
Lỗi:
command not found: gcc: Hệ điều hành chưa nhận dạng được bộ compiler. Hãy đảm bảo bạn đã cài đặt thành công Xcode Command Line Tools (trên macOS) hoặc góibuild-essential(trên Linux). -
Lỗi:
implicit declaration of function 'printf': Lỗi này xảy ra khi bạn quên chỉ thị#include <stdio.h>ở đầu file. Compiler sẽ không nhận biết được hàmprintfđến từ đâu.
Thử thách nhỏ dành cho bạn:
Hãy thử chỉnh sửa văn bản bên trong hàm printf("...") thành tên của bạn hoặc bất cứ nội
dung gì bạn thích, lưu lại, biên dịch và chạy lại chương trình xem kết quả nhé!
7. Cờ biên dịch quan trọng (Compiler Flags)
Khi bạn chạy lệnh đơn giản gcc hello.c -o hello, trình biên dịch sẽ biên dịch thành công
mà hầu như không đưa ra bất kỳ cảnh báo nào. Điều này không có nghĩa là mã nguồn của bạn hoàn hảo —
GCC mặc định ẩn rất nhiều cảnh báo hữu ích. Trong thực tế chuyên nghiệp, lập trình viên luôn bật các
cờ (flags) để buộc compiler phân tích mã nguồn nghiêm ngặt hơn, giúp phát hiện lỗi tiềm ẩn ngay từ
giai đoạn biên dịch thay vì khi chương trình đang chạy.
Bảng dưới đây liệt kê các cờ biên dịch quan trọng nhất mà bạn nên nắm vững:
| Cờ (Flag) | Mô tả chức năng |
|---|---|
-Wall |
Bật hầu hết các cảnh báo phổ biến (biến không sử dụng, thiếu return, so sánh sai kiểu...) |
-Wextra |
Bật thêm các cảnh báo bổ sung mà -Wall chưa bao gồm |
-Werror |
Biến mọi cảnh báo thành lỗi — compiler sẽ dừng biên dịch nếu có warning |
-g |
Nhúng thông tin debug vào file thực thi, phục vụ cho GDB/LLDB và Valgrind |
-O0 |
Không tối ưu hóa (mặc định) — dễ debug nhất vì mã máy ánh xạ trực tiếp với mã nguồn |
-O1, -O2 |
Tối ưu hóa trung bình đến cao — tăng tốc chương trình, -O2 là mức phổ biến cho
bản phát hành
|
-O3 |
Tối ưu hóa tích cực nhất — có thể tăng kích thước file và đôi khi gây lỗi với mã không chuẩn |
-std=c11 / -std=c17 |
Chỉ định phiên bản tiêu chuẩn C sử dụng (C11 hoặc C17) |
-pedantic |
Tuân thủ nghiêm ngặt tiêu chuẩn ISO C, cảnh báo mọi phần mở rộng không chuẩn |
Lệnh biên dịch khuyến nghị khi phát triển (development):
gcc -Wall -Wextra -g -std=c17 hello.c -o hello
Lệnh biên dịch khuyến nghị khi phát hành (release):
gcc -Wall -Wextra -Werror -O2 -std=c17 hello.c -o hello
8. Tự động hóa biên dịch với Makefile
Khi dự án phát triển lớn hơn với nhiều file mã nguồn, việc gõ lại các lệnh gcc dài dòng
mỗi lần biên dịch trở nên cực kỳ bất tiện và dễ sai sót. Makefile là một công cụ tự
động hóa kinh điển giải quyết vấn đề này. Nó cho phép bạn định nghĩa toàn bộ quy trình biên dịch một
lần duy nhất, sau đó chỉ cần gõ lệnh make để thực thi. Makefile còn hỗ trợ biên dịch gia
tăng (incremental build) — chỉ biên dịch lại những file đã thay đổi, tiết kiệm đáng kể thời gian.
Dưới đây là một Makefile mẫu hoàn chỉnh cho dự án C cơ bản:
# Configuration variables
CC = gcc
CFLAGS = -Wall -Wextra -g -std=c17
TARGET = hello
SRCS = hello.c
OBJS = $(SRCS:.c=.o)
# Default rule: build the whole project
all: $(TARGET)
# Link the object files into the executable
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
# Compile each .c file into a .o file
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Clean up build artefacts
clean:
rm -f $(OBJS) $(TARGET)
# Mark targets that are not real files
.PHONY: all clean
Cách sử dụng Makefile rất đơn giản. Mở Terminal tại thư mục chứa Makefile và chạy:
# Build the project
make
# Run the program
./hello
# Remove build output to rebuild from scratch
make clean
Lưu ý: Đối với các dự án lớn hơn với hàng chục hoặc hàng trăm file mã nguồn, bạn nên tìm hiểu CMake — một hệ thống build đa nền tảng mạnh mẽ hơn, có khả năng tự động sinh ra Makefile phù hợp với từng hệ điều hành.
9. Gỡ lỗi cơ bản với GDB (GNU Debugger)
Khi chương trình chạy sai kết quả hoặc bị crash, việc đặt printf khắp nơi để kiểm tra giá
trị biến là cách làm thủ công và kém hiệu quả. GDB (GNU Debugger) là công cụ gỡ lỗi
chuyên nghiệp cho phép bạn dừng chương trình tại bất kỳ dòng nào, kiểm tra giá trị biến, và đi từng
bước qua từng lệnh để tìm chính xác vị trí lỗi.
GDB đã được cài đặt sẵn khi bạn cài build-essential (Linux) hoặc Xcode Command Line Tools
(macOS). Trên macOS, bạn cũng có thể sử dụng LLDB (lldb ./program) —
trình gỡ lỗi mặc định của Apple với giao diện lệnh tương tự.
Chương trình hello.c ở mục 4 không có biến nào và chỉ chạy đúng một lệnh, nên không có gì
để quan sát. Hãy tạo thêm một file debug_demo.c có biến thay đổi theo thời gian — đó mới
là thứ GDB giúp bạn nhìn thấy:
#include <stdio.h>
int main() {
int total = 0;
for (int i = 1; i <= 5; i++) {
total = total + i;
}
printf("Total = %d\n", total);
return 0;
}
Chạy bình thường, chương trình in ra Total = 15. Điều ta muốn thấy là
total lớn dần qua từng vòng lặp — chính là thứ không nhìn được nếu không có debugger.
Bước 1: Biên dịch chương trình với cờ -g (nhúng thông tin debug) và
-O0 (tắt tối ưu hóa):
gcc -g -O0 debug_demo.c -o debug_demo
Bước 2: Khởi chạy GDB với file thực thi:
gdb ./debug_demo
Bước 3: Sử dụng các lệnh GDB cơ bản. Bảng dưới đây tổng hợp các lệnh thiết yếu nhất:
| Lệnh GDB | Chức năng |
|---|---|
run |
Chạy chương trình từ đầu |
break main |
Đặt điểm dừng (breakpoint) tại hàm main |
break 15 |
Đặt breakpoint tại dòng số 15 |
next |
Thực thi dòng tiếp theo (bỏ qua nội dung bên trong hàm con) |
step |
Thực thi dòng tiếp theo (đi vào bên trong hàm con nếu có) |
print x |
In giá trị hiện tại của biến x |
backtrace |
Hiển thị ngăn xếp lời gọi hàm (call stack) — hữu ích khi chương trình crash |
continue |
Tiếp tục chạy cho đến breakpoint tiếp theo hoặc kết thúc |
quit |
Thoát khỏi GDB |
Ví dụ quy trình gỡ lỗi cơ bản:
$ gdb ./debug_demo
(gdb) break main # Dung lai ngay khi vao main
(gdb) run # Chay — chuong trinh dung o dong dau main
(gdb) next # Chay dong "int total = 0;"
(gdb) print total # -> $1 = 0
(gdb) next # Chay het mot vong lap dau tien
(gdb) print total # -> $2 = 1 (0 + 1)
(gdb) next
(gdb) print total # -> $3 = 3 (1 + 2)
(gdb) continue # Chay not phan con lai
(gdb) quit # Thoat GDB
Dãy 0 → 1 → 3 chính là thứ đáng giá ở đây: bạn đang nhìn biến total đổi giá
trị sau từng vòng lặp, mà không phải thêm một dòng printf nào rồi lại xoá đi. Ký hiệu
$1, $2 là cách GDB đánh số các kết quả đã in, để bạn tham chiếu lại về sau.
gdb, và bản cài thêm cần ký chứng chỉ mới chạy được. Dùng
lldb ./debug_demo cho nhẹ: lệnh gần như tương đương — b main,
run, next, frame variable total, continue,
quit.
hello.c thành file thực thi mã máy có tên là
hello trên Terminal?
Bình luận