Trong lập trình nhúng nói chung và cụ thể là lập trình ngôn ngữ C/C++, có thể khi đọc các source lớn như source code kernel linux được viết ta sẽ dễ dàng bắt gặp họ sử dụng các syntax lạ lẫm bên cạnh những dòng code hay các hàm mà ta quen thuộc ( như hình bên dưới). Vậy các bạn có từng tự đặt câu hỏi hay tìm hiểu những dòng define lạ này mang ý nghĩa gì, và tại sao chúng lại được sử dụng hay không. Trong bài viết ngày hôm nay, mình sẽ giới thiệu cho các bạn các khái niệm mà ít khi được sử dụng trong GNU C là predefine Macro và Function attribute.

I. Macro trong GNU C
1. Giới thiệu tổng quát về Macro
Macro trong GNU C là các định nghĩa được xử lý trong giai đoạn đầu tiên của quá trình biên dịch một chương trình, hay còn gọi là quá trình tiền xử lí ( preprocessing) của GCC. Chúng giúp định nghĩa các hằng số thực hiện thay thế các đoạn mã giúp cho mã ngắn gọn dễ bảo trì hơn.
Macro trong GNU C và GNU Fortran đóng vai trò quan trọng trong việc tối ưu mã nguồn, hỗ trợ điều kiện biên dịch và giúp lập trình viên dễ dàng bảo trì mã hơn. Trong khi macro của GNU C mạnh mẽ và linh hoạt hơn với khả năng mở rộng cú pháp, macro trong GNU Fortran chủ yếu được sử dụng để định nghĩa hằng số và kiểm soát điều kiện biên dịch.
2. Một số Macro thông dụng
2.1 __GNUC__, __GNUC_MINOR__ và __GNUC_PATCHLEVEL__
Macro được trình biên dịch GNU Compiler Collection (GCC) định nghĩa tự động. Macro này giúp xác định rằng mã nguồn đang được biên dịch bằng GCC phiên bản nào. Trong đó lần lượt __GNUC__ sẽ mang giá trị version major number, __GNUC_MINOR__ sẽ mang giá trị minor number và __GNUC_PATCHLEVEL__ sẽ mang giá trị patchlevel của trình biên dịch GCC được sử dụng để biên dịch chương trình. Ta có thể sử dụng bộ macro này để giới hạn chương trình chỉ được biên dịch với GCC với version lớn hơn phiên bản nhất định. Ví dụ bên dưới khi đặt code của chương trình trong block #if thì chương trình chỉ được biên dịch và chạy khi GCC version lớn hơn 3.2.0 nếu không thì sẽ đoạn code trong block #if sẽ không được chạy
/* Test for GCC > 3.2.0 */
#if __GNUC__ > 3 || \
(__GNUC__ == 3 && (__GNUC_MINOR__ > 2 || \
(__GNUC_MINOR__ == 2 && \
__GNUC_PATCHLEVEL__ > 0)))
2.2 __STRICT_ANSI__
Macro do trình biên dịch GCC định nghĩa khi biên dịch với tùy chọn -std=cXX hoặc -ansi. Nó có tác dụng giới hạn trình biên dịch chỉ sử dụng các tính năng được định nghĩa trong chuẩn ANSI C hoặc standard C(ISO C), vô hiệu hóa các mở rộng không thuộc chuẩn của GCC.
#include <stdio.h>
int main() {
#ifdef __STRICT_ANSI__
printf("ANSI C is using!\n");
#else
printf("GCC is using extensions not belong to ANSI!\n");
#endif
return 0;
}
Khi chương trình được compile bằng lệnh gcc -std=c99 strict_test.c -o strict_test khi chạy sẽ báo là “ANSI C is using!”. Nhưng nếu biên dịch mà không có -std=c99, nó sẽ báo “GCC is using extensions not belong to ANSI”.
2,3 __FILE_NAME__
Ý nghĩa: Macro chứa tên file mã nguồn đang được biên dịch hiện tại (không bao gồm đường dẫn). Nếu file mã nguồn đang được biên dịch nằm ở /src/main.c thì FILE_NAME sẽ là main.c.
#include <stdio.h>
int main() {
printf("__FILE_NAME__: %s\n", __FILE_NAME__);
return 0;
}
2.4 __INCLUDE_LEVEL__
Ý nghĩa: là một macro đặc biệt trong GCC giúp xác định mức độ lồng nhau của file header trong quá trình biên dịch. Khi trình biên dịch biên dịch một file .c và gặp các include, nó sẽ tăng cấp độ include (INCLUDE_LEVEL) mỗi khi đi sâu vào một file header. Nghĩa là giá trị của __INCLUDE_LEVEL__ là:
- 0 nếu được gọi ở file nguồn chính (.c)
- 1 nếu đang ở file header được include trực tiếp từ file chính
- 2 nếu file header này lại include một file header khác, v.v.
#include <stdio.h>
#include "header1.h"
int main() {
printf("Main file: __INCLUDE_LEVEL__ = %d\n", __INCLUDE_LEVEL__);
return 0;
}
Khi chương trình trên được biên dịch giá trị __INCLUDE_LEVEL__ được in sẽ là 0 và nếu header1.h có in ra __INCLUDE_LEVEL__ của nó thì sẽ là 1.
2.5 __ELF__
- ELF (Executable and Linkable Format) là định dạng file thực thi chính trên các hệ điều hành Unix-like, bao gồm Linux, BSD, Solaris.
- __ELF__ trong GCC, dùng để xác định rằng định dạng file thực thi (binary format) của của mã nguồn sau khi biên dịch có phải là ELF (Executable and Linkable Format) hay không.
- Nếu hệ thống đang sử dụng định dạng ELF, macro __ELF__ sẽ được định nghĩa (thường là 1). Nếu hệ thống không sử dụng ELF (ví dụ: định dạng COFF, Mach-O), macro này sẽ không được định nghĩa.
#include <stdio.h>
int main() {
#ifdef __ELF__
printf("System using ELF format binary\n");
#else
printf("System not using ELF format binary\n");
#endif
return 0;
}
2.6 __VERSION__
Ý nghĩa: Được định nghĩa sẵn trong trình biên dịch GCC, chứa chuỗi phiên bản của GCC đang được sử dụng để biên dịch chương trình.
#include <stdio.h>
int main() {
printf("GCC version: %s\n", __VERSION__);
return 0;
}
2.7 __OPTIMIZE__
- __OPTIMIZE__ là một macro đặc biệt trong GCC, được định nghĩa khi trình biên dịch đang áp dụng các tối ưu hóa (-O1, -O2, -O3, -Os, …). Nó giúp kiểm tra xem mã nguồn có được biên dịch với tối ưu hóa hay không.
- Nếu trình biên dịch không sử dụng tối ưu hóa (-O0 hoặc không đặt cờ -O), __OPTIMIZE__ sẽ không được định nghĩa.
- Nếu trình biên dịch sử dụng bất kỳ mức tối ưu hóa nào (-O1, -O2, -O3, -Os, -Ofast), __OPTIMIZE__ sẽ được định nghĩa.
#include <stdio.h>
int main() {
#ifdef __OPTIMIZE__
printf("Optimizations are enabled.\n");
#else
printf("No optimizations.\n");
#endif
return 0;
}
Khi biên dịch với:
- gcc -O2 optimize_test.c: Xuất ra “Optimizations are enabled.”
- gcc -O0 optimize_test.c: Xuất ra “No optimizations.”.
2.8 __NO_INLINE__
Ý nghĩa:
- Định nghĩa nếu trình biên dịch vô hiệu hóa inline function (-O0, -fno-inline)
- Nếu trình biên dịch được chạy với tùy chọn -fno-inline, macro __NO_INLINE__ sẽ được định nghĩa
- Nếu không có tùy chọn -fno-inline, macro này sẽ không được định nghĩa
- -fno-inline là một tùy chọn của GCC dùng để vô hiệu hóa việc tự động inline của trình biên dịch. Khi sử dụng tùy chọn này, GCC sẽ không tự động biến các hàm inline thành mã nội tuyến, ngay cả khi bạn khai báo inline trong mã nguồn.
- Mặc định, GCC sẽ tự động inline các hàm nếu nó cho rằng điều đó sẽ cải thiện hiệu suất. Khi sử dụng -fno-inline, GCC sẽ buộc tất cả các hàm phải giữ nguyên dạng hàm gọi (function call) thay vì nội tuyến.
#include <stdio.h>
inline void test_inline() {
printf("This is an inline function.\n");
}
int main() {
#ifdef __NO_INLINE__
printf("Inlining is disabled.\n");
#else
test_inline();
#endif
return 0;
}
Kết quả khi biên dịch:
- gcc -O0 -fno-inline: Xuất ra “Inlining is disabled.”.
- gcc -O2: Hàm test_inline() có thể được inline và xuất ra “This is an inline function.”
2.9 __CHAR_UNSIGNED__
Ý nghĩa:
- Theo standard C, kiểu dữ liệu char có thể là signed hoặc unsigned, tùy thuộc vào kiến trúc phần cứng và trình biên dịch.
- CHAR_UNSIGNED là một macro của GCC, được định nghĩa nếu kiểu char mặc định trên hệ thống là unsigned thay vì signed.
- Nếu char mặc định là signed, macro này sẽ không được định nghĩa.
- Chủ yếu được sử dụng trong limits.h.
#include <stdio.h>
int main() {
#ifdef __CHAR_UNSIGNED__
printf("char is unsigned by default.\n");
#else
printf("char is signed by default.\n");
#endif
return 0;
}
2.10 __REGISTER_PREFIX__
Ý nghĩa:
- Mỗi kiến trúc CPU có thể sử dụng các ký hiệu khác nhau để đặt tên thanh ghi trong assembly
- Một số kiến trúc (như x86/x86-64) không có tiền tố
- Một số kiến trúc khác (như ARM, RISC-V) có thể cần tiền tố
- GCC sử dụng REGISTER_PREFIX để giúp mã inline assembly dễ dàng hoạt động trên nhiều kiến trúc khác nhau.
- Dùng để viết assembly inline có thể chạy trên nhiều nền tảng
Ví dụ: Khi viết inline assembly trong GCC, nếu code cần chạy trên nhiều kiến trúc khác nhau, bạn
có thể sử dụng REGISTER_PREFIX để tránh lỗi cú pháp.
#include <stdio.h>
int main() {
int a = 10, b;
asm volatile (
"movl " __REGISTER_PREFIX__ "eax, " __REGISTER_PREFIX__ "ebx"
: "=b"(b)
: "a"(a)
);
printf("b = %d\n", b);
return 0;
}
2.11 __DEPRECATED
Ý nghĩa:
- __DEPRECATED là một macro trong GCC (và nhiều trình biên dịch khác) dùng để đánh dấu một hàm, biến, hoặc bất kỳ phần mã nào là deprecated (không được khuyến khích sử dụng nữa và có thể bị loại bỏ trong các phiên bản tương lai).
- Khi một phần mã được đánh dấu là deprecated, trình biên dịch sẽ cảnh báo khi phần mã đó được sử dụng, giúp lập trình viên nhận biết và thay thế các phần mã cũ, không còn duy trì.
ví dụ : Khi một phần mã được đánh dấu là deprecated, trình biên dịch sẽ cảnh báo khi phần mã đó
được sử dụng, giúp lập trình viên nhận biết và thay thế các phần mã cũ, không còn duy trì.
#include <stdio.h>
__attribute__((deprecated)) int x;
__attribute__((deprecated)) void foo() {
printf("This function is deprecated\n");
}
int main() {
foo();
x = 10;
return 0;
}
Khi sử dụng một hàm, biến, hoặc phần mã được đánh dấu là deprecated, trình biên dịch sẽ đưa ra cảnh
báo như sau: warning: ‘foo’ is deprecated [-Wdeprecated-declarations].
2.12 __EXCEPTIONS
Ý nghĩa: __EXCEPTIONS là một macro được GCC sử dụng để xác định liệu cơ chế xử lý ngoại lệ (exception handling) có được bật trong chương trình hay không. Nếu cơ chế ngoại lệ được bật, mã sẽ xử lý các lỗi hoặc sự kiện đặc biệt (ngoại lệ) theo cách mà lập trình viên chỉ định (ví dụ: qua try, catch trong C++).
Trong nhiều ngôn ngữ lập trình như C++, ngoại lệ cho phép bạn xử lý các lỗi hoặc tình huống bất thường trong khi chương trình đang thực thi. Cơ chế này bao gồm các từ khóa như:
- try: Để bao bọc đoạn mã có thể gây ra lỗi
- catch: Để xử lý lỗi khi nó xảy ra
- throw: Để ném ngoại lệ khi có lỗi
Ví dụ:
try {
int x = 10 / 0;
} catch (const std::exception& e) {
std::cout << "Exception caught: " << e.what() << std::endl;
}
- Định nghĩa nếu trình biên dịch C++ hỗ trợ ngoại lệ (try-catch).
- Không được định nghĩa nếu sử dụng -fno-exceptions.
Ví dụ:
#include <iostream>
int main() {
#ifdef __EXCEPTIONS
try {
throw "Exception";
} catch (const char* msg) {
std::cout << "Caught: " << msg << std::endl;
}
#else
std::cout << "Exceptions are disabled." << std::endl;
#endif
return 0;
}
II. Function attribute trong GNU C
1. Giới thiệu tổng quát về Function attribute trong GNU C
Thuộc tính hàm trong GNU C là các tính chất đặc biệt có thể được sử dụng để cung cấp thông tin bổ sung cho trình biên dịch về hành vi của hàm. Những thuộc tính này giúp tối ưu hóa việc thực thi hàm, áp đặt các ràng buộc và cải thiện quá trình debug. Thuộc tính hàm cho phép các lập trình viên chỉ định tối ưu hóa, khả năng hiển thị, căn chỉnh bộ nhớ và quy ước gọi hàm, giúp source code hiệu quả hơn và có thể hoạt động được trên các nền tảng khác nhau.
2. Các Function attribute được sử dụng thường xuyên
2.1 Thuộc tính access
2.1.1 Giới thiệu
Thuộc tính access trong GCC giúp phát hiện các truy cập không hợp lệ hoặc không an toàn của các hàm đối với tham số kiểu con trỏ (hoặc trong C++, tham chiếu). Nếu một hàm vi phạm cách truy cập đã định, trình biên dịch có thể cảnh báo bằng các lỗi như:
- -Wstringop-overflow (tràn bộ đệm khi thao tác chuỗi),
- -Wuninitialized (sử dụng biến chưa khởi tạo).
- -Wunused (biến không được sử dụng)
2.1.2 Cú pháp
__attribute__ ((access (access-mode, ref-index)))
__attribute__ ((access (access-mode, ref-index, size-index)))
- access-mode: Chỉ định cách truy cập vào con trỏ, có thể là:
- read_only: Chỉ đọc
- read_write: Đọc và ghi
- write_only: Chỉ ghi.
- none: Không truy cập
- ref-index: Xác định tham số con trỏ cần kiểm tra, tính từ 1.
- size-index (tùy chọn): Xác định tham số kiểu số nguyên chứa kích thước tối đa của vùng nhớ được truy cập.
2.1.3 Chế độ truy cập
read_only
- Hàm chỉ đọc dữ liệu từ vùng nhớ mà con trỏ trỏ đến, không được phép sửa đổi.
- Nếu con trỏ trỏ đến dữ liệu chưa khởi tạo, sẽ có cảnh báo.
- Tương tự const, nhưng mạnh hơn vì không thể cast để bỏ thuộc tính này.
Ví dụ:
__attribute__ ((access (read_only, 1)))
int puts (const char*);
Hàm puts sử dụng con trỏ const char* để đọc nhưng không ghi.
read_write
- Cho phép đọc và ghi vào đối tượng mà con trỏ trỏ tới
- Nếu vùng nhớ chưa khởi tạo, sẽ có cảnh báo.
Ví dụ
__attribute__ ((access (read_write, 1), access (read_only, 2)))
char* strcat (char*, const char*);
Hàm strcat ghi vào tham số đầu tiên (read_write) và chỉ đọc từ tham số thứ hai (read_only).
write_only
Chỉ ghi dữ liệu vào con trỏ, không đọc từ nó. Ví dụ:
__attribute__ ((access (write_only, 1), access (read_only, 2)))
char* strcpy (char*, const char*);
Trong hàm strcpy, con trỏ đầu tiên là write_only (được ghi nhưng không đọc), còn con trỏ thứ hai là read_only (chỉ đọc).
none
Chỉ định rằng con trỏ không được sử dụng để truy cập đối tượng mà nó trỏ đến. Ví dụ:
__attribute__ ((access (none, 1, 2)))
void* example (void*, size_t);
2.2 Thuộc tính alias
Thuộc tính alias dùng để tạo bí danh (alias) cho một hàm hoặc biến khác. Khi một symbol được khai báo với alias, nó trở thành một “tên thay thế” cho symbol gốc. Với điều kiện symbol được đặt alias phải có cùng kiểu dữ liệu hoặc chữ kí hàm với symbol gốc, symbol gốc phải được khai báo trước trong cùng một file được biên dịch.
#include <stdio.h>
void __f () {
printf("Function __f() called\n");
}
// create alias f() for __f()
void f () __attribute__ ((weak, alias ("__f")));
int main() {
f(); // call f(), but __f() will be run
return 0;
}
2.3 Thuộc tính aligned trong GCC
Thuộc tính aligned trong GCC cho phép chỉ định căn chỉnh bộ nhớ (memory alignment) của hàm, biến hoặc trường trong struct.
- Đối với hàm, nó đảm bảo hàm bắt đầu ở một địa chỉ bộ nhớ có căn chỉnh nhất định (theo byte).
- Đối với biến hoặc trường trong struct, nó đảm bảo dữ liệu nằm trên một địa chỉ căn chỉnh phù hợp, giúp tối ưu hiệu suất truy xuất.
- Lưu ý (alignment) phải là số nguyên có dạng lũy thừa của 2 (1, 2, 4, 8, 16, . . . ).
- Việc căn chỉnh sẽ giúp tối ưu CPUI caching khi đọc mã và thực thi.
#include <stdio.h>
// function will be put at address is multiple of 16
void foo() __attribute__((aligned(16)));
void foo() {
printf("Function foo is aligned to 16-byte boundary\n");
}
int main() {
foo();
return 0;
}
2.4 Thuộc tính always_inline
always_inline là một attribute trong GCC dùng để yêu cầu biên dịch luôn luôn inline (tích hợp vào mã nguồn) hàm, bất kể các tùy chọn tối ưu hóa khác như thế nào.
- Inline là một kỹ thuật tối ưu hóa nơi mà hàm được thay thế trực tiếp vào vị trí gọi của nó, thay vì tạo ra một lời gọi hàm truyền thống.
- Hàm có thể không được inline nếu trình biên dịch xác định rằng việc inline sẽ không mang lại lợi ích về hiệu suất hoặc nếu nó quá phức tạp.
- Với always_inline, bất kỳ yêu cầu inline nào của hàm sẽ được thực thi ngay cả khi tối ưu hóa không được bật hoặc khi có một số hạn chế khác đối với việc inline.
void foo() __attribute__((always_inline));
void foo() {
// Do something
}
2.5 Thuộc tính const
- const attribute trong C/C++ được sử dụng để thông báo cho trình biên dịch rằng một hàm có giá trị trả về không thay đổi dựa trên các thay đổi của trạng thái chương trình (observable state). Hàm này chỉ phụ thuộc vào giá trị của các đối số mà nó nhận và sẽ trả về giá trị giống nhau mỗi khi được gọi với cùng một bộ đối số.
- Việc khai báo hàm với const giúp trình biên dịch thực hiện một số tối ưu hóa như elimination of common subexpressions (loại bỏ các biểu thức lặp lại) và giảm số lần gọi hàm không cần thiết khi cùng một đối số được sử dụng nhiều lần trong chương trình.
- Khi một hàm được khai báo với const, trình biên dịch biết rằng các cuộc gọi hàm với cùng một giá trị đối số có thể được thay thế bằng kết quả trả về của lần gọi hàm đầu tiên mà không cần thực hiện lại hàm đó.
int square (int x) __attribute__ ((const));
Hàm square(int x) sẽ luôn trả về giá trị giống nhau nếu đối số x không thay đổi, bất kể có những câu lệnh nào giữa các lần gọi hàm. Điều này giúp trình biên dịch có thể tối ưu hóa các cuộc gọi lặp lại hàm, tránh tính toán lại giá trị đã biết.
2.6 Thuộc tính constructor và destructor
constructor và destructor là các attributes trong GCC được sử dụng để chỉ định các hàm sẽ được gọi tự động trong các giai đoạn cụ thể của chương trình. Cụ thể như sau:
- constructor: Hàm này sẽ được gọi tự động trước khi chương trình vào hàm main().
- destructor: Hàm này sẽ được gọi tự động sau khi main() kết thúc hoặc sau khi exit() được gọi.
Ví dụ : Nếu một hàm được đánh dấu với thuộc tính là constructor nó sẽ được gọi trước khi hàm main thực thi.
#include <stdio.h>
void my_constructor() __attribute__((constructor));
void my_destructor() __attribute__((destructor));
void my_constructor() {
printf("my_constructor called\n");
}
void my_destructor() {
printf("my_destructor called\n");
}
int main() {
printf("my main fucntion called\n");
return 0;
}
Hàm được đánh dấu với destructor sẽ được gọi sau khi chương trình hoàn thành, tức là sau khi hàm main() kết thúc hoặc khi hàm exit() được gọi.
2.7 Thuộc tính optimize
- optimize là một attribute trong GCC dùng để chỉ định các tùy chọn tối ưu hóa riêng biệt cho một hàm, khác với các tùy chọn tối ưu hóa đã được chỉ định trên dòng lệnh khi biên dịch toàn bộ chương trình.
- Cho phép chỉ định các mức tối ưu hóa (optimization level) hoặc các tùy chọn tối ưu hóa chi tiết cho từng hàm riêng biệt trong mã nguồn mà không ảnh hưởng đến các phần khác của chương trình
- Có thể áp dụng optimize attribute vào một hàm và cung cấp các đối số để chỉ định mức độ tối ưu hóa hoặc các tùy chọn tối ưu hóa chi tiết.
void my_function() __attribute__((optimize("O2,funroll-loops")));
void my_function() __attribute__((optimize("O2,inline-functions")));