Hiểu về Kỹ Thuật Chèn Định (Interpositioning) trong Ngôn ngữ C

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

Thẻ: interpositioning C programming function overriding shared libraries LD_PRELOAD

Đăng vào ngày 30 tháng 6 lúc 18:57