前言
在《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_ 等