我想更改程序在文件系统上实际打开的某些路径的路径。原因是我想并行运行一个程序,但该程序用作/tmp/somedir/
其临时目录,并且并行实例遇到冲突。
我发现这个很好的答案可以做到这一点:是否可以伪造进程的特定路径?。可悲的是,虽然这适用于cat
广告,但它不适用于我的程序。我认为原因是程序使用了C++ API。
为了重现,我首先制作了一个非常简单的程序,在文件中写入一些内容:
#include <fstream>
#include <string_view>
#include <iostream>
int main() {
std::ofstream myfile;
myfile.open("test.log");
std::string_view text{"hello world\n"};
myfile.write(text.data(), text.size());
return 0;
}
然后我使用strace
并在最后看到了这个:
brk(NULL) = 0x558b5d5e3000
brk(0x558b5d604000) = 0x558b5d604000
futex(0x7f94e2e7e77c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3, "hello world\n", 12) = 12
close(3) = 0
exit_group(0) = ?
所以看起来是调用了C api,但是使用的函数是openat
.
我还看到了 C so 库的这个,稍后会涉及到:
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
所以我openat
除了实现之外open
还实现了,这是完整的程序。出于测试目的,我没有更改路径,而是将其记录到文件中。
/*
* capture calls to a routine and replace with your code
* g++ -Wall -O2 -fpic -shared -ldl -lstdc++ -o fake_open_file.so fake_open_file.cpp
* LD_PRELOAD=/home/myname/fake_open_file.so cat
*/
#define _FCNTL_H 1 /* hack for open() prototype */
#undef _GNU_SOURCE
#define _GNU_SOURCE /* needed to get RTLD_NEXT defined in dlfcn.h */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <mutex>
#include <fstream>
#include <string_view>
#include <iostream>
// for the test, I just log anything that was open into a new log file
struct open_reporter
{
open_reporter() = default;
void report_filename(std::string_view filename)
{
std::lock_guard<std::mutex> l{file_report_mutex};
if(!is_open) {
myfile.open("/home/myname/fileopen.log");
}
std::string tmp = std::string{filename} + "\n";
myfile.write(tmp.data(), tmp.size());
}
std::ofstream myfile;
std::mutex file_report_mutex;
bool is_open = false;
};
static open_reporter reporter_;
extern "C" {
int open(const char *pathname, int flags, mode_t mode)
{
static int (*real_open)(const char *pathname, int flags, mode_t mode) = nullptr;
if (!real_open) {
real_open = reinterpret_cast<decltype(real_open)>(dlsym(RTLD_NEXT, "open"));
char *error = dlerror();
if (error != nullptr) {
reporter_.report_filename("ERROR OCCURED!");
reporter_.report_filename(error);
exit(1);
}
}
reporter_.report_filename(pathname);
return real_open(pathname, flags, mode);
}
int openat(int dirfd, const char *pathname, int flags, mode_t mode)
{
static int (*real_openat)(int dirfd, const char *pathname, int flags, mode_t mode) = nullptr;
if (!real_openat) {
real_openat = reinterpret_cast<decltype(real_openat)>(dlsym(RTLD_NEXT, "openat"));
char *error = dlerror();
if (error != nullptr) {
reporter_.report_filename("ERROR OCCURED!");
reporter_.report_filename(error);
exit(1);
}
}
reporter_.report_filename(pathname);
return real_openat(dirfd, pathname, flags, mode);
}
}
这适用于cat
静止,但不适用于我的测试程序。即使我更改open
并openat
返回 0,虽然这会中断,但cat
它不会对我的测试程序产生任何影响。我还检查了这些符号是否在我的二进制文件中:
$ nm -gD fake_open_file.so | grep open
0000000000001470 W _ZN13open_reporterD1Ev
0000000000001470 W _ZN13open_reporterD2Ev
0000000000001450 T open
0000000000001460 T openat
我可以看到这两个功能都存在。查看 C 库,我看到了差异,但我不知道它意味着什么。我编辑了不是open
或 的内容openat
:
$ nm -gD /lib/x86_64-linux-gnu/libc.so.6 |grep open
0000000000114820 W openat@@GLIBC_2.4
0000000000114820 W openat64@@GLIBC_2.4
0000000000114690 W open@@GLIBC_2.2.5
0000000000114690 W open64@@GLIBC_2.2.5
0000000000114690 W __open@@GLIBC_2.2.5
0000000000114690 W __open64@@GLIBC_2.2.5
00000000001147c0 T __open64_2@@GLIBC_2.7
0000000000119b80 T __open64_nocancel@@GLIBC_PRIVATE
0000000000114660 T __open_2@@GLIBC_2.7
0000000000040800 T __open_catalog@@GLIBC_PRIVATE
0000000000119b80 T __open_nocancel@@GLIBC_PRIVATE
0000000000114950 T __openat64_2@@GLIBC_2.7
00000000001147f0 T __openat_2@@GLIBC_2.7
除了内容之外@@GLIBC
,这些都是相同的。我以前从未这样做过,所以这就是我的调试能力。我在这里问而不是这样,因为这是我得到原始答案的地方,而且这看起来更像是Linux知识而不是编程问题,程序本身非常简单。
答案1
背景:C
你的strace
输出显示...
brk(NULL) = 0x558b5d5e3000
brk(0x558b5d604000) = 0x558b5d604000
futex(0x7f94e2e7e77c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3, "hello world\n", 12) = 12
close(3) = 0
exit_group(0) = ?
...但strace
跟踪系统调用,而不是函数调用...以及通过LD_PRELOAD
使用的函数插入功能来电。对于 C 程序,该openat
调用可能是通过open
或调用的open64
。例如,如果我开始这样的事情:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main() {
int fd;
int nb;
if (-1 == (fd = open("test.log", O_RDWR|O_CREAT, 0666))) {
perror("open");
exit(1);
}
if (-1 == (nb = write(fd, "hello world\n", 12))) {
perror("write");
exit(1);
}
printf("write %d bytes\n", nb);
return 0;
}
我在输出中看到strace
这称为:
openat(AT_FDCWD, "test.log", O_RDWR|O_CREAT, 0666) = 3
但是,如果我尝试像您一样使用 覆盖openat
,LD_PRELOAD
我会看到相同的行为:它不起作用。但是,如果我拦截该open
调用:
int open(const char *pathname, int flags, mode_t mode) {
static int (*real_open)(const char *, int, mode_t);
fprintf(stderr, "OPEN PATH: %s\n", pathname);
if (!real_open) {
real_open = (dlsym(RTLD_NEXT, "open"));
char *error = dlerror();
if (error != NULL) {
fprintf(stderr, "ERROR OCCURED! %s\n", error);
exit(1);
}
}
return real_open(pathname, flags, mode);
}
然后效果很好:
$ LD_PRELOAD=./fakeopen.so ./c_example
OPEN PATH: test.log
write 12 bytes
更复杂:C++
C++ 代码的情况有点复杂,因为当你编写......
myfile.open("test.log");
...到底什么是被调用?如果我们查看 的输出LD_DEBUG=symbols ./your_program
,我们会看到如下内容:
420239: symbol=_ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode; lookup in file=./cc_main [0]
420239: symbol=_ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode; lookup in file=/lib64/libstdc++.so.6 [0]
所以实际的函数调用是损坏的名字像_ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode
.我们可以像任何其他函数一样覆盖它。如果我们创建wrapper.cc
:
#include <iostream>
extern "C" {
int _ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode() {
{
std::cerr << "This is the wrapped open method\n";
return 0;
}
}
}
并将其编译为wrapper.so
:
g++ -shared -fPIC -o wrapper.so wrapper.cc
然后我们可以将它与您的简单程序一起使用:
LD_PRELOAD=./wrapper.so ./your_program
并得到输出:
$ LD_PRELOAD=./wrapper.so ./your_program
This is the wrapped open method
这样,我们就成功地包装了该open
方法!为了让它成功调用真正的方法,您需要弄清楚函数签名实际上_ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode
是什么样的。我不是 C++ 人员,我无法回答这个问题,但希望这可以帮助您取得进步。
杂项注释
可以使用 C++ 执行函数重载,这将允许您使用常规函数名称而不是损坏的名称(并且您不需要猜测 C++ 函数的 C 原型是什么样子)。
这在中进行了一些讨论stackoverflow 上的这个问题。