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, LinuxWindows.

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:

  1. 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.
  2. 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.
  3. 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ỉ.
  4. 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ụ printf khô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.
ℹ️ Chưa cần thuộc ngay
Bốn cái tên trên chỉ cần hiểu đại ý ở thời điểm này. Đến mục 5 bạn sẽ tự tay chạy từng giai đoạn một và mở từng file trung gian ra xem — lúc đó máy đã có trình biên dịch và đã có file 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):

  1. Mở ứng dụng Terminal (nhấn Cmd + Space, gõ "Terminal" và nhấn Enter).
  2. Nhập lệnh sau và nhấn Enter:
Terminal
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:

Terminal (Ubuntu/Debian)
sudo apt update
sudo apt install build-essential

Đối với Fedora/CentOS/RHEL:

Terminal (Fedora)
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:

PowerShell (quyền quản trị)
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.

ℹ️ Vì sao không dùng thẳng MinGW hay Visual Studio
Cả hai đều dịch được C và không có gì sai. Nhưng chúng khác Unix ở những chi tiết mà series này có chạm tới: đường dẫn dùng \ 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:

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.

  1. 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.
  2. Cài đặt ứng dụng và mở VS Code lên.
  3. 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).

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!".

  1. Mở một thư mục trống bằng VS Code (File -> Open Folder).
  2. Tạo một file mới tên là hello.c.
  3. Nhập đoạn mã nguồn chuẩn mực sau vào file:
hello.c
#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 file stdio.h vào đây. Đó là nơi khai báo printf. Thiếu dòng này thì trình biên dịch không biết printf là gì.
  • int main() { ... }hàm main là đ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ên main. 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ước main chí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:

Terminal
gcc hello.c -o hello

2. Chạy chương trình vừa được biên dịch xong:

Terminal
./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ói build-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àm printf đế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):

Terminal
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):

Terminal
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:

Makefile
# 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:

Terminal
# 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:

debug_demo.c
#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):

Terminal
gcc -g -O0 debug_demo.c -o debug_demo

Bước 2: Khởi chạy GDB với file thực thi:

Terminal
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:

Phiên GDB mẫu
$ 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.

💡 Trên macOS dùng LLDB
macOS không cài sẵn 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.
📝 Kiểm tra kiến thức bài 1
Lệnh nào sau đây dùng để biên dịch file hello.c thành file thực thi mã máy có tên là hello trên Terminal?

Bài viết liên quan trong series

Bài 2: Cú pháp C cơ bản, Biến, Kiểu dữ liệu & Nhập xuất Quay lại Lộ trình C Series