C语言中的Interpositioning(函数插入)

发布于:2025-03-21 ⋅ 阅读:(34) ⋅ 点赞:(0)

前言

        在《C专家编程》这本书里面,有以下内容:

        这个函数插入,应用范围之广,以及编译器竟然真的不报错,让我引发了这个兴趣,然后告诉AI,让他给我几个示例,让我理解:

示例1:strlen

观察一下这个代码:

#include <stdio.h>
#include <string.h>

// 全局计数器
static int strlen_count = 0;

#if 1
// 重写strlen函数
size_t strlen(const char *s) {
	strlen_count++;
	
	// 手动实现strlen
	size_t count = 0;
	while (s[count] != '\0') {
		count++;
	}
	
	printf("strlen被调用第%d次,字符串:'%s',长度:%zu\n", 
		strlen_count, s, count);
	return count;
}
#endif

int main() {
	char *str1 = "Hello";
	char *str2 = "World";
	
	printf("str1长度: %zu\n", strlen(str1));
	printf("str2长度: %zu\n", strlen(str2));
	
	return 0;
}

如果这里的  #if 1 改成 #if 0,那么结果是:

str1长度: 5
str2长度: 5

如果是 #if 1,那么相当于是把 "string.h"里面的strlen函数给替换成自己的了,结果如下:

会发现,自己的那个strlen被调用了3次:

这个问题的关键在于程序在执行过程中,在我们看不到的地方还调用了一次strlen函数。让我详细解释:

第一次strlen调用(意外的调用):

  • 这次调用是由C运行时库(CRT)在程序启动时自动进行的
  • 它在获取程序自身的可执行文件路径时调用了strlen
  • 这就是为什么第一次打印显示了你的程序的完整路径:C:\Users\jl.yu\Documents\Project\C++\test\test.exe

后面的调用是我们代码中明显的调用:

  • 第二次调用:strlen(str1) 处理 "Hello"
  • 第三次调用:strlen(str2) 处理 "World"

要避免看到第一次系统调用的输出,你可以这样修改代码:

#include <stdio.h>
#include <string.h>

// 全局计数器
static int strlen_count = 0;
static int first_call = 1;  // 添加一个标志

// 重写strlen函数
size_t strlen(const char *s) {
    strlen_count++;
    
    // 手动实现strlen
    size_t count = 0;
    while (s[count] != '\0') {
        count++;
    }
    
    // 跳过第一次系统调用的输出
    if (!first_call) {
        printf("strlen被调用第%d次,字符串:'%s',长度:%zu\n", 
            strlen_count-1, s, count);
    }
    first_call = 0;
    
    return count;
}

示例2:链接时插入,替换malloc和free

// wrapper.c
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>

// 包装malloc函数
void *malloc(size_t size) {
    void *(*original_malloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
    void *ptr = original_malloc(size);
    printf("malloc(%zu) = %p\n", size, ptr);
    return ptr;
}

// 包装free函数
void free(void *ptr) {
    void (*original_free)(void*) = dlsym(RTLD_NEXT, "free");
    printf("free(%p)\n", ptr);
    original_free(ptr);
}

编译和使用方法:

gcc -shared -fPIC wrapper.c -o libwrapper.so -ldl
LD_PRELOAD=./libwrapper.so ./your_program

示例3:运行时插入,文件操作监控器

写一个track.c

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <time.h>
#include <string.h>

// 记录文件操作
static int file_ops_count = 0;

// 包装fopen
FILE *fopen(const char *path, const char *mode) {
	FILE *(*original_fopen)(const char*, const char*) = dlsym(RTLD_NEXT, "fopen");
	
	file_ops_count++;
	time_t now = time(NULL);
	char *time_str = ctime(&now);
	time_str[strlen(time_str)-1] = '\0';  // 移除换行符
	
	printf("[%s] fopen(%s, %s) - 操作计数: %d\n", 
		time_str, path, mode, file_ops_count);
	
	return original_fopen(path, mode);
}

// 包装fclose
int fclose(FILE *fp) {
	int (*original_fclose)(FILE*) = dlsym(RTLD_NEXT, "fclose");
	
	time_t now = time(NULL);
	char *time_str = ctime(&now);
	time_str[strlen(time_str)-1] = '\0';
	
	printf("[%s] fclose(%p)\n", time_str, (void*)fp);
	
	return original_fclose(fp);
}

// 包装fread
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
	size_t (*original_fread)(void*, size_t, size_t, FILE*) = 
	dlsym(RTLD_NEXT, "fread");
	
	size_t result = original_fread(ptr, size, nmemb, stream);
	printf("fread: 请求读取 %zu 个元素,实际读取 %zu 个元素\n", 
		nmemb, result);
	
	return result;
}

再写一个test.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

	// 声明文件指针

	FILE *fp = NULL;

	// 打开文件

	// 使用"rb"表示以二进制读取模式打开文件

	fp = fopen("test.txt", "rb");

	if (fp == NULL) {

		printf("文件打开失败!\n");

		return -1;
	}


	// 获取文件
	fseek(fp, 0, SEEK_END);    // 将文件指针移到文件末尾
	long fileSize = ftell(fp);  // 获取文件大小
	fseek(fp, 0, SEEK_SET);    // 将文件指针重新移到文件开头

	
	// 分配内存buffer
	char *buffer = (char *)malloc(fileSize + 1);  // +1 用于存储字符串结束符
	if (buffer == NULL) {
		printf("内存分配失败!\n");
		fclose(fp);
		return -1;
	}

	memset(buffer, 0, fileSize + 1);  // 将buffer初始化为0

	
	// 读取文件内容
	size_t readSize = fread(buffer, 1, fileSize, fp);
	if (readSize != fileSize) {
		printf("文件读取失败!实际读取:%zu,预期读取:%ld\n", readSize, fileSize);
		free(buffer);
		fclose(fp);
		return -1;
	}

	
	// 打印文件内容
	printf("文件内容:\n%s\n", buffer);
	printf("文件大小:%ld 字节\n", fileSize);


	// 清理资源
	free(buffer);  // 释放内存
	fclose(fp);    // 关闭文件

	return 0;

}

进行编译和测试:

gcc -shared -fPIC track.c -o libtrack.so -ldl
echo hello world >> test.txt
echo hello world >> test.txt

LD_PRELOAD=./libtrack.so ./test

./test

总结:

        Interpositioning对于大佬,可以干很多高级操作,但是对于普通人,很容易犯错而不知道原因。所以这也是为什么一定要避免函数重名的原因,当然,为了防止重名,也可以在自己的函数前加一些前缀,就向gst_ /  av_  等