有没有办法改变 fiddle 的工作目录?

Is there a way to change the working directory of fiddle?

我正在尝试使用 Fiddle 在 Ruby 中加载 C 共享库。

这是一个最小的例子:

require 'fiddle'
require 'fiddle/import'

module Era
  extend Fiddle::Importer

  dlload './ServerApi.so'

  extern 'int era_init_lib()'
  extern 'void era_deinit_lib()'
  extern 'int era_process_request(const char* request, char** response)'
  extern 'void era_free(char* response)'
end

Era.era_init_lib
begin
  # ...
ensure
  Era.era_deinit_lib
end

共享库加载没有问题。但是,当我调用 Era.era_init_lib 时,它会尝试加载其他库(Network.soProtobuf.so)。我将这些文件放在当前工作目录中(与 ServerApi.so 在同一目录中)。

然而,当我尝试执行上面的代码时,我收到以下错误:

! Failed to load library: /home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so, error: /home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so: cannot open shared object file: No such file or directory

如果我将文件放在错误描述的位置,一切正常。

我的猜测是 fiddle 的 C 工作目录与 Ruby 工作目录不同。 我想将项目文件保留在项目而不是 Ruby 安装目录。

如何使用我的项目文件夹中的 Network.so

所有*.so文件均由第三方提供。我没有源代码,因此无法更改这些文件。函数签名由 documentation.

提供

strace 中搜索 Network.so 得到这些结果:

readlink("/proc/self/exe", "/home/username/.rvm/rubies/ruby-2."..., 4096) = 44
openat(AT_FDCWD, "/home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
futex(0x7fcc16666d90, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x7fcc16b44520, FUTEX_WAKE_PRIVATE, 2147483647) = 0
write(2, "! Failed to load library: ", 26! Failed to load library: ) = 26
write(2, "/home/username/.rvm/rubies/ruby-2."..., 50/home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so) = 50
write(2, ", error: ", 9, error: )                = 9
write(2, "/home/username/.rvm/rubies/ruby-2."..., 109/home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so: cannot open shared object file: No such file or directory) = 109
write(2, "\n", 1)                       = 1

我还写了一个 C 脚本,它做同样的事情,当文件被放入同一目录时,它工作得很好。所以这可能是库的错误,我假设它检查当前 运行ning 程序的位置,然后尝试从该文件夹加载库。这将解释 运行 作为 Ruby 脚本时的行为(因为它 运行s 作为 Ruby 程序的一部分),而 C 二进制文件 运行s独立。


对于那些想要重新创建 (Linux) 问题的人。您可以从 here 下载必要的文件。这给你 server-linux-x86_64.sh 文件。

支持的发行版有:Suse、Ubuntu、Debian、Red Hat 和 CentOS,但其他发行版也可以正常工作。

您可以 运行 安装程序,它将文件放在 /opt/eset/RemoteAdministrator/Server 中。或者,假设你们中的大多数人不想安装完整的应用程序,您可以 运行 以下命令:

sed '1,/^# Start of TAR\.GZ file #$/d' server-linux-x86_64.sh | sed '1d' > server-linux-x86_64.tar.gz

从 .sh 文件中删除所有安装程序指令,只留下二进制 .tar.gz 数据,将其写入 server-linux-x86_64.tar.gz.

将文件 ServerApi.soProtobuf.soNetwork.so 复制到您喜欢的目录中。在同一目录和 运行 脚本中创建一个 Ruby 脚本(带有问题代码)。

如果我们读取 strace 输出,我们会看到该库从 /proc/self/exe 获取当前可执行文件位置,然后从那里搜索后续库。

/proc/self/exe 不容易修改,但是通过使用硬 link 到当前目录中的 Ruby 可执行文件,我们可以欺骗它指向一个新文件夹。

问题是制作困难 link 需要 root。

无论如何,这是一个独立的解决方案(请注意,它会在您第一次 运行 时询问 root 密码,以创建硬盘 link)。

将其放在脚本的顶部:

# Obtain path to current executable
exe = File.readlink("/proc/self/exe")

# Check if we are running the hard-liked version
if !exe.match /localruby/
  if !File.exist?('localruby')
    # Create a hard link to the current Ruby exe using sudo
    system("sudo ln #{exe} localruby")
  end

  puts "Restarting..."

  # In order to prevent infinite busy loop in case of some mishap
  sleep 1 

  # Rerun self using the hard-linked Ruby executable.
  # This will make /proc/self/exe point to the hard-link, which then
  # allows the ESET library to search for .so files in current folder.
  exec('./localruby', File.expand_path(__FILE__))
end

require 'fiddle'
require 'fiddle/import'

# ...rest of your script goes here...

没有任何额外 Ruby 代码的简单解决方案是手动创建硬 link,然后始终 运行 使用 ./localruby myscript.rb 脚本,而不是使用正常 ruby myscript.rb.

因为ServerApi.so检查/proc/self/exe所有后续文件加载的位置,用正常方法修改这个目标非常困难,直接修改ServerApi.so 本身,以便它使用 proc 以外的其他内容作为来源。

如果我们 运行 strings ServerApi.so,我们可以验证要检查的位置是否存储在 ServerApi.so:

中的字符串中
strings ServerApi.so | grep 'proc/self/exe'
B/proc/self/exe

所以现在我们需要做的就是将这个字符串修改为适合我们的其他内容。

修改字符串的最简单方法是将其替换为与原始长度完全相同的内容。这样我们就不必担心更改字符串末尾零填充或意外更改 ServerApi.so.

的总大小

在这里我们可以看到合适的候选人可能是 /tmp/scriptexe:

/proc/self/exe
/tmp/scriptexe   <- same length

所以让我们这样做:

sed -e 's/proc\/self\/exe/tmp\/scriptexe/' ServerApi.so > ServerApi_Mod.so

现在我们可以验证更改:

strings ServerApi_Mod.so | grep scriptexe
B/tmp/scriptexe

接下来我们需要创建 /tmp/scriptexe 以实际指向我们的 Ruby 脚本:

ln -s /the/full/path/to/our/ruby/script.rb /tmp/scriptexe

然后我们修改我们的脚本:

dlload './ServerApi_Mod.so

现在我们可以 运行 正常了:

ruby script.rb

一切都应该正常。