Chuyển tới nội dung chính

Trình biên dịch cl (MSVC)

cl.exe là trình biên dịch C/C++ chính thức của Microsoft, đi kèm với Visual Studio (hoặc Build Tools for Visual Studio). Trình biên dịch này tuân theo hệ sinh thái MSVC và sử dụng cú pháp Windows-style (với dấu /) thay vì UNIX-style (- như gcc/clang).

Nó tích hợp chặt chẽ với linker link.exe, các trình tối ưu như /O2, /GL, và hệ thống debug PDB.

Cú pháp tổng quát

cl [file.cpp] [cờ tiền xử lý] [cờ biên dịch] [cờ cảnh báo] [cờ tối ưu] [cờ include] [/link [cờ linker]]
Gợi ý thứ tự
  • Các cờ compiler nhóm theo chức năng: preprocess, biên dịch, cảnh báo, tối ưu, include, ...
  • /link nên đặt cuối lệnh, vì mọi thông số sau đó sẽ được chuyển sang linker link.exe

Các nhóm cờ và giải thích

Dưới đây sẽ liệt kê các cờ cơ bản và phổ biến đại diện cho mỗi nhóm cờ trong cú pháp tổng quát.

  1. Cờ tiền xử lý
    • /D<macro>: Định nghĩa macro
    • /U<macro>: Xóa macro
    • /FI<file>: Yêu cầu trình biên dịch tự động include một file header (.h/.hpp) vào đầu mỗi file nguồn trong quá trình tiền xử lý
    • /E: Chỉ tiền xử lý, in ra stdout
    • /showIncludes: In danh sách file header

  2. Cờ biên dịch / cấu hình
    • /c: Chỉ biên dịch, không link
    • /Fo:<file.obj>: Tên file đối tượng đầu ra
    • /Fe:<file.exe>: Tên file thực thi đầu ra
    • /LD: Tạo DLL (tương đương /link /DLL)
    • /MD, /MT: Liên kết runtime C++ dạng DLL hoặc static
    • /EHsc: Bật exception C++
    • /GR-: Tắt RTTI
    • /m32//m64: Chỉ định kiến trúc 32-bit hoặc 64-bit khi biên dịch
    • /Zc:__cplusplus: Đảm bảo macro __cplusplus đúng chuẩn

  3. Cờ cảnh báo
    • /W3, /W4, /Wall: Mức độ cảnh báo
    • /WX: Chuyển cảnh báo thành lỗi
    • /permissive-: Tuân thủ nghiêm C++ standard
    • /std:c++20: Chọn chuẩn C++

  4. Cờ tối ưu hóa
    • /Od: Tắt tối ưu (debug)
    • /O1, /O2, /Ox: Tối ưu hiệu năng
    • /GL: Bật Link-Time Code Generation (LTO)
    • /Gw: Tối ưu global
    • /GS-: Tắt stack check (không khuyến nghị)

  5. Cờ include
    • /I<dir>: Thêm thư mục include
    • /external:I<dir>: External header, ý nghĩa tương tự cờ -isystem <dir> của g++, cảnh báo trong các file header ở đây sẽ bị bỏ qua.
    • /external:W0: Tắt cảnh báo external

  6. Cờ linker (sau /link)
    • /link /OUT:<file.exe>: Tên file exe
    • /IMPLIB:<file.lib>: Tạo import library khi build DLL
    • /DLL: Xác nhận build DLL
    • /LIBPATH:<dir>: Thư mục chứa .lib
    • /SUBSYSTEM:<arg>: Chỉ định loại chương trình: console, windows, native,... — ảnh hưởng đến entry point và hiển thị CMD.
    • mylib.lib, user32.lib,...: Các file lib liên kết
ghi chú

Lựa chọn cờ đầu ra: /Fe: hay /link /OUT:?

  • Nên ưu tiên dùng /Fo:<file.obj>/Fe:<file.exe> để đặt tên file đầu ra ngay trong trình biên dịch cl
  • Tránh dùng /link /OUT: trừ khi bạn cần điều khiển linker rất chi tiết (export symbol, PDB, DEF file...)
  • Các cờ như /Fe:, /Fo: tích hợp tốt với các cờ debug hoặc chia nhỏ build theo file
  • Sử dụng /Fe: rõ ràng hơn, dễ dùng hơn và là best practice khi biên dịch theo phong cách MSVC
danh sách cờ tham khảo

Trình liên kết của cl

Trình liên kết mặc định trong hệ sinh thái MSVClink.exe, được tích hợp sẵn khi sử dụng trình biên dịch cl.exe. Đây là linker của Microsoft, hoạt động tối ưu cho định dạng PE/COFF trên Windows.

  • Tương thích chặt chẽ với cl.exe: Mọi lệnh biên dịch không có /c sẽ tự động gọi link.exe để thực hiện liên kết.
  • Hỗ trợ sinh nhiều loại file đầu ra:
    • File thực thi .exe
    • Thư viện động .dll + import library .lib
    • Thư viện tĩnh .lib
    • File debug .pdb kèm symbol
  • Tối ưu cho hệ sinh thái Windows:
    • Tích hợp tốt với Visual Studio và MSBuild
    • Hỗ trợ chuẩn gọi hàm Windows (__stdcall, __cdecl, __fastcall)
    • Hỗ trợ file .def để xuất symbol cụ thể
  • Hiệu suất ổn định và tối ưu hóa runtime:
    • Tương thích với Link-Time Code Generation (/GL, /LTCG)
    • Có thể sinh file .map để phân tích bố cục bộ nhớ chương trình
    • Tối ưu hóa kích thước và tốc độ khi kết hợp với /O2, /OPT:REF, /OPT:ICF
  • Không tương thích với linker GNU hoặc LLVM (như ld, lld):
    • link.exe sử dụng cú pháp và flag riêng biệt (Windows-style)
    • Không dùng được với file .o, .a kiểu ELF
thông tin
  • Mọi tùy chọn dành cho link.exe (như /OUT:, /DLL, /LIBPATH:, /DEF:,...) phải được đặt sau /link khi gọi từ dòng lệnh cl.exe.
  • Khi dùng với clang-cl, link.exe vẫn là linker mặc định nếu không cấu hình lld-link hoặc linker khác.

Ví dụ

Biên dịch file thực thi .exe:

cl main.cpp utils.cpp /std:c++20 /Fe:build\myapp.exe /W4 /O2 /Iinclude  /link /LIBPATH:lib mylib.lib

Giải thích:

  • File nguồn: main.cpp, utils.cpp
  • Cờ biên dịch: /std:c++20, /Fe:build\myapp.exe (Chọn chuẩn C++ và chỉ định file đầu ra)
  • Cờ cảnh báo: /W4 (Cấu hình mức cảnh báo)
  • Cờ tối ưu: /O2 (Tối ưu hóa tốc độ)
  • Cờ include: /Iinclude (Thêm thư mục chứa header)
  • Từ khóa bắt buộc để chuyển phần còn lại cho linker: /link
  • Cờ linker: /LIBPATH:lib mylib.lib (Chỉ định thư mục và thư viện để liên kết)

Biên dịch và sử dụng thư viện tĩnh (.lib)

1. Biên dịch mã nguồn thành file đối tượng (.obj)

  • Mục đích: Biên dịch các file .cpp thành file đối tượng .obj để chuẩn bị đóng gói thành thư viện .lib
  • Cú pháp tổng quát:
    cl /c <file.cpp> /Fo:<file.obj> [cờ biên dịch khác]
  • Ví dụ áp dụng:
    cl /c mylib.cpp /Fo:build\mylib.obj /std:c++20 /W4 /EHsc

2. Đóng gói file đối tượng thành thư viện tĩnh (.lib)

  • Mục đích: Tạo thư viện tĩnh .lib từ một hoặc nhiều file .obj
  • Cú pháp tổng quát:
    lib <file.obj> [/OUT:<file.lib>]
  • Ví dụ áp dụng:
    lib build\mylib.obj /OUT:lib\mylib.lib
  • Kiểm tra thư viện sau khi đóng gói: Sau khi tạo .dll.lib, bạn có thể dùng các công cụ dòng lệnh của MSVC để kiểm tra thông tin trong các file thư viện.
    • Liệt kê symbol trong .lib hoặc .dll

      dumpbin /EXPORTS lib\mylib.dll
      • Hiển thị danh sách các hàm được export.
      • Dùng để xác nhận các hàm đã được export đúng (hữu ích nếu bạn không dùng .def hoặc gặp lỗi unresolved symbol).
      dumpbin /SYMBOLS lib\mylib.lib
      • Hiển thị symbol trong import library .lib, xác định các tên hàm mà linker sẽ dùng khi bạn biên dịch chương trình chính.
    • Hiển thị thông tin toàn bộ file

      dumpbin /ALL lib\mylib.dll > dllinfo.txt
      • Ghi toàn bộ thông tin chi tiết về cấu trúc file .dll vào file dllinfo.txt, bao gồm:
        • Header PE
        • Import table, export table
        • Base relocation, debug info (nếu có)
    • Liệt kê các file .obj trong thư viện .lib

      lib /LIST lib\mylib.lib
      • Tương tự llvm-ar -t, dùng để xác nhận thư viện .lib chứa những file .obj nào.
    • Trích xuất .obj từ .lib (nếu cần)

      lib /EXTRACT:mylib.obj lib\mylib.lib
      • Trích xuất một file .obj từ thư viện .lib. Có thể dùng để kiểm tra hoặc debug riêng file .obj.
    lưu ý
    • Các công cụ này như dumpbin.exelib.exe đều đi kèm với Developer Command Prompt for Visual Studio.
    • Nếu bạn không tìm thấy lệnh dumpbin, hãy mở terminal đúng môi trường hoặc thêm đường dẫn đến VC\Tools\... vào PATH.

3. Liên kết thư viện .lib với chương trình chính

  • Mục đích: Biên dịch chương trình chính (.cpp) và liên kết với thư viện tĩnh .lib để tạo file thực thi
  • Cú pháp tổng quát:
    cl <main.cpp> [cờ biên dịch khác] /I<dir_include> /Fe:<output.exe> /link /LIBPATH:<dir_lib> <libname.lib>
  • Ví dụ áp dụng:
    cl main.cpp /std:c++20 /W4 /O2 /Iinclude /Fe:bin\myapp.exe /link /LIBPATH:lib mylib.lib
    • main.cpp[file.cpp]
    • /std:c++20, /Fe:...[cờ biên dịch]
    • /W4[cờ cảnh báo]
    • /O2[cờ tối ưu]
    • /Iinclude[cờ include]
    • /link → tách nhóm linker
    • /LIBPATH:lib, mylib.lib[cờ linker]

Biên dịch và sử dụng thư viện động (.dll)

1. Biên dịch mã nguồn thành file đối tượng (.obj)

  • Mục đích: Biên dịch file nguồn (.cpp) thành file đối tượng (.obj) để sử dụng trong quá trình tạo DLL.

  • Cú pháp tổng quát:

    cl /c <file.cpp> /Fo:<file.obj> [cờ biên dịch khác]
  • Ví dụ áp dụng:

    cl /c src\mylib.cpp /Fo:build\mylib.obj /std:c++20 /W4 /EHsc /DBUILD_MYLIB
    • /c: Chỉ biên dịch, không liên kết (tạo .obj)
    • src\mylib.cpp: File nguồn cần biên dịch
    • /Fo:build\mylib.obj: Chỉ định tên file đối tượng đầu ra
    • /std:c++20: Biên dịch theo tiêu chuẩn C++20
    • /W4: Bật mức cảnh báo cao (Level 4)
    • /EHsc: Bật xử lý ngoại lệ C++ (try-catch), không hỗ trợ SEH
    • /DBUILD_MYLIB: Định nghĩa macro BUILD_MYLIB (tương đương #define BUILD_MYLIB)
  • Header file cần thiết:

    • Sử dụng __declspec(dllexport) khi build DLL, và __declspec(dllimport) khi sử dụng DLL trong chương trình khác.
    • Dùng macro để gom cả 2 tình huống:
      #ifdef BUILD_MYLIB
      #define MYLIB_API __declspec(dllexport)
      #else
      #define MYLIB_API __declspec(dllimport)
      #endif

      extern "C" MYLIB_API int add(int a, int b);
Calling convention là gì?
  1. Calling convention là gì?
    Calling convention (quy ước gọi hàm) là quy tắc quy định:

    • Thứ tự truyền tham số
    • Ai chịu trách nhiệm thu dọn stack (caller hay callee)
    • Cách đặt tên hàm (name mangling) (nếu có)
  2. Các loại phổ biến trong Windows (x86)

    Tên gọiTừ khóaTruyền tham sốThu dọn stackDùng phổ biến ở đâu
    Cdecl__cdeclTừ phải sang tráiCallerMặc định trong C/C++
    Stdcall__stdcallTừ phải sang tráiCalleeWindows API, AutoIt, VB6
    Fastcall__fastcallMột số dùng thanh ghiCalleeHiệu năng cao (ít dùng)
    Thiscall__thiscallthis trong ecx, còn lại trên stackCalleeMặc định với hàm thành viên C++

    a. __cdecl

    • Caller (người gọi) phải clean stack sau khi gọi hàm.
    • Cho phép overload số lượng tham số (printf, scanf)
    • Thường dùng mặc định trong C/C++ compile
      __cdecl int add(int a, int b);

    b. __stdcall

    • Callee (hàm được gọi) sẽ tự động dọn stack.
    • Thường dùng cho Windows API, DLL để gọi từ ngôn ngữ khác
    • Gọi từ AutoIt, VB6, Pascal, C# (P/Invoke) → nên dùng __stdcall
      __stdcall int add(int a, int b);  // gọn hơn cho người gọi

    c. __fastcall

    • Truyền 2 tham số đầu tiên qua thanh ghi (ecx, edx) → nhanh hơn.
    • Stack dọn bởi callee.
    • Thường không cần thiết trừ khi tối ưu hóa chuyên sâu.
      __fastcall int add(int a, int b);

    d. __thiscall (mặc định cho C++ member function)

    • this nằm trong thanh ghi ecx
    • Các tham số còn lại trên stack
    • Không dùng được trong extern "C" (vì C không có this)

Nếu không khai báo rõ __stdcall, thì hàm export từ DLL sẽ dùng calling convention mặc định, thường là:

  • __cdecl trên Windows x86
  • Trên Windows x64: calling convention mặc định là Microsoft x64 ABI, không cần khai báo (vì tất cả dùng chung)

TÌNH HUỐNG:

  • Nếu đang dùng Windows 64-bit:
    • Không có __stdcall hay __cdecl phân biệt nữa
    • Tất cả hàm gọi giữa module đều theo x64 calling convention (được thống nhất)
    • Chương trình khác (64-bit) vẫn gọi được hàm dù không khai báo __stdcall
  • Nhưng nếu viết DLL 32-bit và chương trình khác (32-bit) sử dụng DLL này:
    • Thì phải chỉ định rõ __stdcall; Nếu không, sẽ xảy ra:
      • Lỗi stack khi trả về từ hàm
      • Treo chương trình hoặc kết quả sai

2. Tạo thư viện động (.dll) và import library (.lib)

  • Mục đích: Tạo .dll từ file .obj, đồng thời sinh file import .lib để dùng cho liên kết tĩnh (implicit linking).
  • Cú pháp tổng quát:
    cl /LD <file.obj> /Fe:<file.dll> /link /IMPLIB:<file.lib> [tùy chọn linker khác]
  • Ví dụ áp dụng:
    cl /LD build\mylib.obj /Fe:lib\mylib.dll /link /IMPLIB:lib\mylib.lib
    • /LD: Yêu cầu tạo thư viện động .dll (tự động bật /DLL khi chuyển cho linker)
    • build\mylib.obj: File đối tượng được liên kết thành .dll
    • /Fe:lib\mylib.dll: Đặt tên file đầu ra là mylib.dll trong thư mục lib\
    • /link: Bắt đầu nhóm cờ dành cho linker (link.exe)
    • /IMPLIB:lib\mylib.lib: Tạo file import library .lib để hỗ trợ liên kết tĩnh (implicit linking)

3. Sử dụng thư viện động (.dll)

  1. Liên kết tĩnh (Implicit Linking)

    • Cú pháp tổng quát:
      cl <main.cpp> [cờ biên dịch khác] /I<dir_include> /Fe:<output.exe> /link /LIBPATH:<dir_lib> <import.lib>
    • Yêu cầu:
      • File .lib là import library (do /IMPLIB: sinh ra)
      • File .h phải dùng __declspec(dllimport) khi include
      • File .dll phải tồn tại khi chạy chương trình
    • Lưu ý quan trọng:
      • mylib.dll phải nằm cùng thư mục với .exe, hoặc phải khai báo trong %PATH%
      • Có thể dùng API như SetDllDirectory() để bổ sung thư mục DLL tại runtime
  2. Tải động (Explicit Linking)

    • Không cần mylib.lib hay mylib.h, nhưng cần:
      • Biết tên hàm và prototype
      • Dùng LoadLibrary, GetProcAddress
    • Cú pháp biên dịch:
      cl main.cpp /Fe:myapp.exe
    • Mã ví dụ:
      #include <windows.h>
      #include <iostream>

      typedef int (*AddFunc)(int, int);

      int main() {
      HMODULE h = LoadLibrary("lib\\mylib.dll");
      if (!h) return 1;
      AddFunc add = (AddFunc)GetProcAddress(h, "add");
      if (!add) return 2;
      std::cout << "2 + 3 = " << add(2, 3) << "\n";
      FreeLibrary(h);
      }