Giới thiệu
Kỹ thuật chèn định (interpositioning) trong C là một phương thức mạnh mẽ cho phép lập trình viên ghi đè các hàm thư viện chuẩn bằng cách định nghĩa lại chúng trong chương trình của mình. Điều này tuy hữu ích nhưng cũng tiềm ẩn nhiều rủi ro nếu không được hiểu rõ.
Ví dụ 1: Tùy biến hàm strlen
Xem xét đoạn mã sau:
#include
#include
// Bộ đếm toàn cục
static int dem_goi_strlen = 0;
#if 1
// Triển khai lại hàm strlen
size_t do_dai_chuoi(const char *s) {
dem_goi_strlen++;
// Tính toán độ dài chuỗi
size_t kich_thuoc = 0;
while (s[kich_thuoc] != '\0') {
kich_thuoc++;
}
printf("Hàm strlen được gọi lần thứ %d, chuỗi: '%s', độ dài: %zu\n",
dem_goi_strlen, s, kich_thuoc);
return kich_thuoc;
}
#endif
int main() {
const char *chuoi1 = "Xin chào";
const char *chuoi2 = "Thế giới";
printf("Độ dài chuoi1: %zu\n", do_dai_chuoi(chuoi1));
printf("Độ dài chuoi2: %zu\n", do_dai_chuoi(chuoi2));
return 0;
}
Nếu #if 1 được đổi thành #if 0, chương trình sẽ sử dụng hàm strlen gốc từ thư viện:
str1 độ dài: 5
str2 độ dài: 5
Nếu #if 1 được giữ nguyên, hàm strlen gốc sẽ bị thay thế bởi hàm tự định nghĩa:
Quan sát thấy hàm tự định nghĩa được gọi 3 lần:
Lần gọi đầu tiên (không mong muốn):
- Được thư viện runtime C tự động thực thi khi khởi động chương trình
- Xảy ra khi hệ thống lấy đường dẫn thực thi của chương trình
Các lần gọi tiếp theo là từ code chính:
- Lần thứ hai: do_dai_chuoi(chuoi1) xử lý "Xin chào"
- Lần thứ ba: do_dai_chuoi(chuoi2) xử lý "Thế giới"
Để tránh hiển thị lần gọi hệ thống đầu tiên, có thể sửa code như sau:
#include
#include
// Bộ đếm toàn cục
static int dem_goi_strlen = 0;
static int lan_dau = 1; // Cờ lần đầu tiên
// Triển khai lại hàm strlen
size_t do_dai_chuoi(const char *s) {
dem_goi_strlen++;
// Tính toán độ dài chuỗi
size_t kich_thuoc = 0;
while (s[kich_thuoc] != '\0') {
kich_thuoc++;
}
// Bỏ qua lần gọi hệ thống đầu tiên
if (!lan_dau) {
printf("Hàm strlen được gọi lần thứ %d, chuỗi: '%s', độ dài: %zu\n",
dem_goi_strlen-1, s, kich_thuoc);
}
lan_dau = 0;
return kich_thuoc;
}
Ví dụ 2: Chèn định tại thời gian liên kết, thay thế malloc và free
// bao_dong.c
#define _GNU_SOURCE
#include
#include
#include
// Bao hàm malloc
void *cap_phat_bo_nho(size_t kich_thuoc) {
void (*nguyen_ban_malloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
void *con_tro = nguyen_ban_malloc(kich_thuoc);
if (con_tro != NULL) {
printf("Đã cấp phát %zu byte tại địa chỉ %p\n", kich_thuoc, con_tro);
} else {
printf("Cấp phát bộ nhớ %zu byte thất bại\n", kich_thuoc);
}
return con_tro;
}
// Bao hàm free
void giai_phong_bo_nho(void *con_tro) {
void (*nguyen_ban_free)(void*) = dlsym(RTLD_NEXT, "free");
if (con_tro != NULL) {
printf("Giải phóng bộ nhớ tại địa chỉ %p\n", con_tro);
nguyen_ban_free(con_tro);
} else {
printf("Cố gắng giải phóng con trỏ NULL\n");
}
}
Biên dịch và sử dụng:
gcc -shared -fPIC bao_dong.c -o libbao_dong.so -ldl
LD_PRELOAD=./libbao_dong.so ./chuong_trinh_cua_ban
Ví dụ 3: Chèn định tại thời gian chạy, giám sát thao tác tệp
Tạo file theo_doi.c:
#define _GNU_SOURCE
#include
#include
#include
#include
#include
// Bộ đếm thao tác tệp
static int dem_thao_tac_tep = 0;
// Bao hàm fopen
FILE *mo_tep(const char *duong_dan, const char *che_do) {
FILE (*nguyen_ban_fopen)(const char*, const char*) = dlsym(RTLD_NEXT, "fopen");
dem_thao_tac_tep++;
time_t hien_tai = time(NULL);
char *thoi_gian = ctime(&hien_tai);
thoi_gian[strlen(thoi_gian)-1] = '\0'; // Loại bỏ ký tự xuống dòng
printf("[%s] mo_tep(%s, %s) - Thao tác thứ: %d\n",
thoi_gian, duong_dan, che_do, dem_thao_tac_tep);
FILE *kq = nguyen_ban_fopen(duong_dan, che_do);
if (kq == NULL) {
printf("Lỗi mở tệp: %s\n", strerror(errno));
}
return kq;
}
// Bao hàm fclose
int dong_tep(FILE *con_tro) {
int (*nguyen_ban_fclose)(FILE*) = dlsym(RTLD_NEXT, "fclose");
time_t hien_tai = time(NULL);
char *thoi_gian = ctime(&hien_tai);
thoi_gian[strlen(thoi_gian)-1] = '\0';
printf("[%s] dong_tep(%p)\n", thoi_gian, (void*)con_tro);
return nguyen_ban_fclose(con_tro);
}
// Bao hàm fread
size_t doc_tep(void *bo_nho, size_t kich_thuoc, size_t so_luong, FILE *luong) {
size_t (*nguyen_ban_fread)(void*, size_t, size_t, FILE*) =
dlsym(RTLD_NEXT, "fread");
size_t ket_qua = nguyen_ban_fread(bo_nho, kich_thuoc, so_luong, luong);
printf("doc_tep: Yêu cầu đọc %zu phần tử, thực tế đọc %zu phần tử\n",
so_luong, ket_qua);
return ket_qua;
}
Tạo file kiem_tra.cpp:
#include
#include
#include
int main() {
FILE *tep = NULL;
const char *ten_tep = "du_lieu.txt";
// Mở tệp
tep = fopen(ten_tep, "rb");
if (tep == NULL) {
printf("Không thể mở tệp!\n");
return -1;
}
// Lấy kích thước tệp
fseek(tep, 0, SEEK_END);
long kich_thuoc_tep = ftell(tep);
fseek(tep, 0, SEEK_SET);
// Cấp phát bộ nhớ
char *bo_nho = (char *)malloc(kich_thuoc_tep + 1);
if (bo_nho == NULL) {
printf("Cấp phát bộ nhớ thất bại!\n");
fclose(tep);
return -1;
}
memset(bo_nho, 0, kich_thuoc_tep + 1);
// Đọc nội dung tệp
size_t kich_thuoc_doc = fread(bo_nho, 1, kich_thuoc_tep, tep);
if (kich_thuoc_doc != kich_thuoc_tep) {
printf("Đọc tệp thất bại! Đọc được: %zu, Kỳ vọng: %ld\n", kich_thuoc_doc, kich_thuoc_tep);
free(bo_nho);
fclose(tep);
return -1;
}
printf("Nội dung tệp:\n%s\n", bo_nho);
printf("Kích thước tệp: %ld byte\n", kich_thuoc_tep);
// Giải phóng tài nguyên
free(bo_nho);
fclose(tep);
return 0;
}
Biên dịch và kiểm tra:
gcc -shared -fPIC theo_doi.c -o libtheo_doi.so -ldl
echo "xin chao the gioi" >> du_lieu.txt
echo "xin chao the gioi" >> du_lieu.txt
LD_PRELOAD=./libtheo_doi.so ./kiem_tra
Kết luận
Kỹ thuật chèn định (interpositioning) là một công cụ mạnh mẽ cho phép các lập trình viên cao cấp thực hiện nhiều thao tác nâng cao. Tuy nhiên, với người mới bắt đầu, việc sử dụng kỹ thuật này dễ dẫn đến lỗi khó phát hiện. Vì vậy, tốt nhất nên tránh đặt tên trùng với các hàm thư viện chuẩn. Để tránh xung đột tên, có thể thêm tiền tố vào các hàm tự định nghĩa, ví dụ như gstreamer sử dụng "gst_" hay ffmpeg sử dụng "av_".