process_vm_readv/process_vm_writev 函数详解
1. 函数介绍
2. 函数原型
#define _GNU_SOURCE
#include <sys/uio.h>
ssize_t process_vm_readv(pid_t pid,
const struct iovec *local_iov,
unsigned long liovcnt,
const struct iovec *remote_iov,
unsigned long riovcnt,
unsigned long flags);
ssize_t process_vm_writev(pid_t pid,
const struct iovec *local_iov,
unsigned long liovcnt,
const struct iovec *remote_iov,
unsigned long riovcnt,
unsigned long flags);
3. 功能
- process_vm_readv: 从指定进程(pid)的内存中读取数据到当前进程的内存中
- process_vm_writev: 将当前进程的内存数据写入到指定进程(pid)的内存中
- 这两个函数都支持分散/聚集I/O操作,可以同时处理多个不连续的内存区域
- 数据传输直接在用户空间进行,避免了内核缓冲区的复制,提高了性能
4. 参数
共同参数说明:
- pid_t pid: 目标进程的进程ID
- 必须是正在运行的进程
- 调用进程必须有权限访问该进程(相同用户或具有CAP_SYS_PTRACE权限)
- *const struct iovec local_iov: 本地内存区域描述符数组
- 描述当前进程中用于读写操作的内存缓冲区
- 每个iovec结构包含基地址和长度
- unsigned long liovcnt: 本地iovec数组的元素个数
- 指定local_iov数组中有效元素的数量
- *const struct iovec remote_iov: 远程内存区域描述符数组
- 描述目标进程中用于读写操作的内存地址
- 每个iovec结构包含基地址和长度
- unsigned long riovcnt: 远程iovec数组的元素个数
- 指定remote_iov数组中有效元素的数量
- unsigned long flags: 标志位(保留字段)
- 当前必须设置为0
iovec结构体定义:
struct iovec {
void *iov_base; // 内存区域的起始地址
size_t iov_len; // 内存区域的长度(字节数)
};
5. 返回值
成功时:
- 返回实际传输的字节数
- 可能小于请求的总字节数(部分传输)
失败时:
- 返回-1,并设置errno错误码
常见错误码:
- EACCES: 没有权限访问目标进程内存
- EFAULT: 指定的内存地址范围无效
- EINVAL: 参数无效(如flags非0,iovcnt过大等)
- ENOMEM: 内存不足
- EPERM: 没有权限操作目标进程
- ESRCH: 目标进程不存在或已终止
6. 相似函数或关联函数
相似函数:
- readv/writev: 在单个进程内进行分散/聚集I/O操作
- preadv/pwritev: 带偏移量的分散/聚集I/O操作
- ptrace: 更通用的进程调试和控制接口
关联函数:
- kill: 向进程发送信号
- wait/waitpid: 等待子进程状态变化
- getpid/getppid: 获取进程ID信息
7. 示例代码
示例1:基础内存读取示例
#define _GNU_SOURCE
#include <sys/uio.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/**
* 读取目标进程内存的简单示例
* 注意:需要知道目标进程的确切内存地址
*/
int read_target_process_memory(pid_t target_pid, void *remote_addr, size_t size) {
char *local_buffer;
struct iovec local_iov[1];
struct iovec remote_iov[1];
ssize_t bytes_transferred;
// 分配本地缓冲区
local_buffer = malloc(size);
if (!local_buffer) {
perror("malloc failed");
return -1;
}
// 设置本地iovec(当前进程的缓冲区)
local_iov[0].iov_base = local_buffer;
local_iov[0].iov_len = size;
// 设置远程iovec(目标进程的内存地址)
remote_iov[0].iov_base = remote_addr;
remote_iov[0].iov_len = size;
// 执行读取操作
bytes_transferred = process_vm_readv(target_pid,
local_iov, 1, // 本地1个iovec
remote_iov, 1, // 远程1个iovec
0); // flags必须为0
if (bytes_transferred == -1) {
printf("process_vm_readv failed: %s\n", strerror(errno));
free(local_buffer);
return -1;
}
printf("成功读取 %zd 字节数据\n", bytes_transferred);
printf("读取的数据内容: ");
for (size_t i = 0; i < bytes_transferred && i < 50; i++) {
if (local_buffer[i] >= 32 && local_buffer[i] <= 126) {
putchar(local_buffer[i]);
} else {
printf("\\x%02x", (unsigned char)local_buffer[i]);
}
}
printf("\n");
free(local_buffer);
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 4) {
printf("使用方法: %s <目标进程PID> <内存地址> <读取大小>\n", argv[0]);
printf("示例: %s 1234 0x601040 100\n", argv[0]);
return 1;
}
pid_t target_pid = atoi(argv[1]);
unsigned long addr = strtoul(argv[2], NULL, 0);
size_t size = strtoul(argv[3], NULL, 0);
printf("尝试读取进程 %d 的内存地址 0x%lx,大小 %zu 字节\n",
target_pid, addr, size);
return read_target_process_memory(target_pid, (void*)addr, size);
}
示例2:批量内存操作示例
#define _GNU_SOURCE
#include <sys/uio.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define MAX_REGIONS 10
/**
* 批量读取多个内存区域
*/
typedef struct {
void *remote_addr; // 远程地址
size_t size; // 区域大小
char *data; // 读取到的数据
} memory_region_t;
int batch_read_memory_regions(pid_t target_pid, memory_region_t *regions, int count) {
struct iovec local_iov[MAX_REGIONS];
struct iovec remote_iov[MAX_REGIONS];
ssize_t total_bytes;
int i;
// 参数检查
if (count > MAX_REGIONS) {
printf("区域数量超过最大限制 %d\n", MAX_REGIONS);
return -1;
}
// 为每个区域分配本地缓冲区并设置iovec
for (i = 0; i < count; i++) {
regions[i].data = malloc(regions[i].size);
if (!regions[i].data) {
printf("为区域 %d 分配内存失败\n", i);
// 释放已分配的内存
for (int j = 0; j < i; j++) {
free(regions[j].data);
}
return -1;
}
// 设置本地iovec
local_iov[i].iov_base = regions[i].data;
local_iov[i].iov_len = regions[i].size;
// 设置远程iovec
remote_iov[i].iov_base = regions[i].remote_addr;
remote_iov[i].iov_len = regions[i].size;
}
// 执行批量读取
total_bytes = process_vm_readv(target_pid,
local_iov, count,
remote_iov, count,
0);
if (total_bytes == -1) {
printf("批量读取失败: %s\n", strerror(errno));
// 释放所有缓冲区
for (i = 0; i < count; i++) {
free(regions[i].data);
}
return -1;
}
printf("批量读取成功,总共传输 %zd 字节\n", total_bytes);
// 显示每个区域的数据
for (i = 0; i < count; i++) {
printf("区域 %d (地址 0x%lx, 大小 %zu): ",
i, (unsigned long)regions[i].remote_addr, regions[i].size);
for (size_t j = 0; j < regions[i].size && j < 20; j++) {
if (regions[i].data[j] >= 32 && regions[i].data[j] <= 126) {
putchar(regions[i].data[j]);
} else {
printf(".");
}
}
printf("\n");
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("使用方法: %s <目标进程PID>\n", argv[0]);
return 1;
}
pid_t target_pid = atoi(argv[1]);
memory_region_t regions[3];
// 设置要读取的内存区域(实际使用时需要根据目标进程调整地址)
regions[0].remote_addr = (void*)0x601000; // 示例地址1
regions[0].size = 32;
regions[1].remote_addr = (void*)0x601020; // 示例地址2
regions[1].size = 16;
regions[2].remote_addr = (void*)0x601030; // 示例地址3
regions[2].size = 8;
printf("批量读取进程 %d 的多个内存区域\n", target_pid);
if (batch_read_memory_regions(target_pid, regions, 3) == 0) {
printf("批量读取操作完成\n");
// 释放内存
for (int i = 0; i < 3; i++) {
free(regions[i].data);
}
} else {
printf("批量读取操作失败\n");
}
return 0;
}
示例3:内存写入和修改示例
#define _GNU_SOURCE
#include <sys/uio.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
/**
* 写入数据到目标进程内存
*/
int write_to_target_memory(pid_t target_pid, void *remote_addr,
const void *data, size_t size) {
struct iovec local_iov[1];
struct iovec remote_iov[1];
ssize_t bytes_written;
// 设置本地iovec(要写入的数据)
local_iov[0].iov_base = (void*)data; // 强制转换,实际不会修改
local_iov[0].iov_len = size;
// 设置远程iovec(目标地址)
remote_iov[0].iov_base = remote_addr;
remote_iov[0].iov_len = size;
// 执行写入操作
bytes_written = process_vm_writev(target_pid,
local_iov, 1,
remote_iov, 1,
0);
if (bytes_written == -1) {
printf("process_vm_writev 失败: %s\n", strerror(errno));
return -1;
}
printf("成功写入 %zd 字节到进程 %d 的地址 0x%lx\n",
bytes_written, target_pid, (unsigned long)remote_addr);
return 0;
}
/**
* 发送信号给目标进程
*/
int send_signal_to_process(pid_t target_pid, int signal) {
if (kill(target_pid, signal) == -1) {
printf("发送信号失败: %s\n", strerror(errno));
return -1;
}
printf("成功发送信号 %d 到进程 %d\n", signal, target_pid);
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("使用方法: %s <目标进程PID>\n", argv[0]);
return 1;
}
pid_t target_pid = atoi(argv[1]);
char new_message[] = "Hello from process_vm_writev!";
int new_value = 999999;
printf("准备修改进程 %d 的内存\n", target_pid);
// 写入字符串数据(需要知道目标进程的确切地址)
if (write_to_target_memory(target_pid, (void*)0x601060,
new_message, strlen(new_message) + 1) == 0) {
printf("字符串写入成功\n");
}
// 写入整数数据
if (write_to_target_memory(target_pid, (void*)0x601040,
&new_value, sizeof(new_value)) == 0) {
printf("整数写入成功\n");
}
// 发送信号让目标进程重新显示数据
send_signal_to_process(target_pid, SIGUSR1);
return 0;
}
示例4:完整的测试目标进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
// 这些变量的地址将被其他进程读取和修改
int shared_counter = 42;
char shared_message[256] = "Original message from target process";
double shared_double = 3.14159;
volatile int running = 1;
void signal_handler(int sig) {
if (sig == SIGUSR1) {
printf("收到SIGUSR1信号,显示当前数据:\n");
printf(" Counter: %d\n", shared_counter);
printf(" Message: %s\n", shared_message);
printf(" Double: %.5f\n", shared_double);
} else if (sig == SIGINT) {
printf("收到终止信号\n");
running = 0;
}
}
int main() {
printf("=== 目标测试进程 ===\n");
printf("进程PID: %d\n", getpid());
printf("变量地址信息:\n");
printf(" shared_counter 地址: %p\n", &shared_counter);
printf(" shared_message 地址: %p\n", shared_message);
printf(" shared_double 地址: %p\n", &shared_double);
printf("\n");
printf("初始数据:\n");
printf(" Counter: %d\n", shared_counter);
printf(" Message: %s\n", shared_message);
printf(" Double: %.5f\n", shared_double);
printf("\n");
printf("按 Ctrl+C 退出,或等待其他进程发送 SIGUSR1 信号\n");
// 注册信号处理程序
signal(SIGUSR1, signal_handler);
signal(SIGINT, signal_handler);
// 主循环
while (running) {
sleep(1);
}
printf("目标进程退出\n");
return 0;
}
使用注意事项
权限要求:
1. 调用进程和目标进程必须具有相同的用户ID
2. 或者调用进程必须具有 CAP_SYS_PTRACE
权限
3. 目标进程必须正在运行(不能是僵尸进程)
地址获取:
1. 需要知道目标进程的确切内存地址
2. 可以通过调试器、符号表或/proc/[pid]/maps文件获取
3. 地址必须在目标进程的有效地址空间内
错误处理:
1. 始终检查返回值
2. 处理部分传输的情况
3. 确保本地缓冲区足够大
安全考虑:
1. 这些函数可能被恶意程序用于内存篡改
2. 在生产环境中应谨慎使用
3. 系统管理员可以通过安全策略限制使用