Linux 不使用 chroot 临时替换C运行时库

有些时候我们需要在老旧的 Linux 系统上运行一些依赖于较新版本 C 库的应用程序或库,应用程序会因为系统中安装的C库缺少符号还启动失败。解决方法之一就是临时替换使用非系统安装的C运行时库。使用临时C库需要做些什么配置及会带来哪些问题呢?

配置步骤
1. 下载与目标应用程序版本相匹配的临时C库,解压缩到临时位置 A。
2. 需要设置 LD_LIBRARY_PATH 环境变量指向目标临时C库的存储位置 A。
3. 需要通过与临时C库匹配的 ld.so 启用应用程序。因为应用程序默认是链接了一个绝对路径的 ld.so,如 x86_64 是 /lib64/ld-linux-x86-64.so.2

衍生问题
使用临时C库的 ld.so 启动的应用程序执行系统标准命令的子进程出错,原因是因为环境变量 LD_LIBRARY_PATH 被子进程继承,从而导致子进程在执行系统C库的ld.so中加载了版本不匹配的临时C库。

解决办法
在合适的时机清除环境变量 LD_LIBRARY_PATH,最合适的时机应用就是执行目标应用程序 main 函数之前啦。这里又要用到了之前写过的方法 => Linux 平台一种进程代码注入方法

/* fakemain.c
 * Heiher <admin@heiher.info>
 */
 
#include <stdio.h>
#include <stdlib.h>
 
#define __USE_GNU
#include <dlfcn.h>
 
int
__libc_start_main(int (*main)(int, char **, char **),
			int argc, char **ubp_av, void (*init)(void),
			void (*fini)(void), void (*rtld_fini)(void),
			void (*stack_end))
{
	int (*__libc_start_main_real)(int (*main) (int, char **, char **),
				int argc, char **ubp_av, void (*init)(void),
				void (*fini)(void), void (*rtld_fini)(void),
				void (*stack_end));
 
	unsetenv ("LD_PRELOAD");
	unsetenv ("LD_LIBRARY_PATH");
 
	__libc_start_main_real = dlsym(RTLD_NEXT, "__libc_start_main");
 
	return __libc_start_main_real(main, argc, ubp_av, init, fini,
				rtld_fini, stack_end);
}
gcc -fPIC -O3 -shared -o libfakemain.so fakemain.c -ldl

设置环境变量 LD_PRELOAD=/xxx/libfakemain.so,运行目标应用程序在执行 main 之前即会清除 LD_PRELOAD 和 LD_LIBRARY_PATH 变量。

为了方便使用我还写了个 wrapper,使用方法是将真实的目标应用程序 xxx 重命令为 xxx.bin,然后创建个符号链接 xxx 指向 wrapper,执行时直接执行 xxx,wrapper 会自动设置所需要的环境变量。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
 
int
main (int argc, char *argv[])
{
	int i;
	char buf[1024], path[1024];
	char *str, *root, *args[512];
 
	/* get FAKE_ROOT */
	root = getenv ("FAKE_ROOT");
	if (!root) {
		fprintf (stderr, "Please set environment FAKE_ROOT!\n");
		return -1;
	}
 
	/* export PATH */
	str = getenv ("PATH");
	if (!str) {
		fprintf (stderr, "Get environment PATH failed!\n");
		return -2;
	}
	if (NULL == strstr (str, root)) {
		strcpy (buf, root);
		strcat (buf, "/bin:");
		strcat (buf, str);
		if (0 != setenv ("PATH", buf, 1)) {
			fprintf (stderr, "Set environment PATH failed!\n");
			return -3;
		}
	}
 
	/* export LD_PRELOAD */
	strcpy (buf, root);
	strcat (buf, "/lib64/libfakemain.so");
	if (0 != setenv ("LD_PRELOAD", buf, 1)) {
		fprintf (stderr, "Set environment LD_PRELOAD failed!\n");
		return -4;
	}
 
	/* export LD_LIBRARY_PATH */
	strcpy (buf, root);
	strcat (buf, "/lib64");
	if (0 != setenv ("LD_LIBRARY_PATH", buf, 1)) {
		fprintf (stderr, "Set environment LD_LIBRARY_PATH failed!\n");
		return -5;
	}
 
	/* set new path */
	strcpy (path, root);
	strcat (path, "/lib64/ld-2.20.so");
	args[0] = path;
 
	/* set real program path */
	strcpy (buf, root);
	strcat (buf, "/bin/");
	strcat (buf, argv[0]);
	strcat (buf, ".bin");
	args[1] = buf;
 
	/* copy arguments */
	for (i=1; i<argc; i++)
	      args[i+1] = argv[i];
	args[i+1] = NULL;
 
	/* run real program */
	return execv (path, args);;
}
gcc -O3 -o wrapper wrapper.c

Over!

GCC 预定义宏: Endian

#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
// little endian
#elif (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
// big endian
#elif (__BYTE_ORDER__ == __ORDER_PDP_ENDIAN__)
// pdp endian, The bytes in 16-bit words are laid out in
// a little-endian fashion, whereas the 16-bit subwords of
// a 32-bit quantity are laid out in big-endian fashion. 
#endif

Over!

MIPS N32 编译 Firefox 链接 libxu.so 时被 Kill

在 Arch Linux (MIPS N32) 系统中编译 Firefox,链接 libxul.so 过程中 ld 进程被 system 给 kill 了,多次尝试都如此。检查发现是 OOM 触发了 system 将进程 kill 了。增加 swap 后仍然无效,原因是 32-bit 的虚拟地址空间不足了。。。

在这种杯具的情况下,可以启动 ld 参数 –no-keep-memory 临时解决此问题。

--no-keep-memory
ld normally optimizes for speed over memory usage by caching the symbol tables
 of input files in memory. This option tells ld to instead optimize for memory
 usage, by rereading the symbol tables as necessary. This may be required if ld
 runs out of memory space while linking a large executable.

如果还是不行,那就要再加上 -S 或 –strip-debug

-S
--strip-debug
    Omit debugger symbol information (but not all symbols) from the output file.

Over!

Kernel 不支持 timerfd 使用 timeout

VPS 内核版本较低,低到还没实现 timerfd。由于是 OpenVZ 的方案,基本上是无法更新 Kernel 的。有一个必要应用正好要使用 HevLib 库的 HevEventSourceTimeout,此 source 是基于 timerfd 的,临时的解决方法是使用 HevEventSourceSignal 监听 SIGALRM 信号, 使用 alarm 或 ualaram 设置定时器:

#include <stdio.h>
#include <signal.h>
#include <hev-lib.h>
 
static bool
signal_handler (void *data)
{
        printf ("timeout signal\n");
 
        alarm (1);
 
        return true;
}
 
int
main (int argc, char *argv[])
{
        HevEventLoop *loop = NULL;
        HevEventSource *signal = NULL;
 
        loop = hev_event_loop_new ();
 
        signal = hev_event_source_signal_new (SIGALRM);
        hev_event_source_set_callback (signal, signal_handler, NULL, NULL);
        hev_event_loop_add_source (loop, signal);
        hev_event_source_unref (signal);
 
        alarm (1);
 
        hev_event_loop_run (loop);
 
        hev_event_loop_unref (loop);
 
        return 0;
}

Over!

变量运算溢出问题

最近犯了一个低级错误呀,记录一下这个教训吧,大概如下:

struct timespec {
    time_t tv_sec;
    long tv_nsec;
};
struct timespec ts;
uint64_t nsec = ts.tv_sec * 1000000000 + ts.tv_nsec;

也就是将 ts 换算成 nanoseconds,在 64-bit 系统上工作的没有问题,但 32-bit 系统上就杯具了。

这个地方可能会有两个问题:
1. 认为目标变量是 64-bit 就足够了,但乘法运算实际上是 (time_t) (tv.tv_sec * 1000000000),结果有可能溢出了。
2. 认为 time_t 类型在所有环境下都是足够的。(我就是认为它都是 64-bit 的)

Over!

移植 HevLib 到 Android 系统

打算将 HevSocks5Client 移植到 Android 系统,替换原来的 HevTLSTunnel 实现。先要移植 HevLib,原以为只要增加两个文件 Application.mk 和 Android.mk 就可以了。但编译到 hev-event-source-signal.c 的时候就出错了,报 sys/signalfd.h 文件找不到。于是 android-ndk 目录里搜索了一下,还真没有这个文件的。这才意识到有可能 Android 的用户态目前还没开放这些 API。

如果仅仅是用户态没有导出问题应该不太,关键内核要实现了这些功能。还好我的系统中存在 /proc/config.gz 文件,解压查询确认 SIGNALFD、TIMERFD 及 EVENTFD 都是支持。下面的事情就好办了,就是增加几个系统调用的实现问题了。进一步确认了间接系统调用 syscall 函数是实现了的,连汇编都不用写了。

diff --git a/src/hev-event-source-signal.c b/src/hev-event-source-signal.c
index daf3e96..01f0088 100644
--- a/src/hev-event-source-signal.c
+++ b/src/hev-event-source-signal.c
@@ -11,7 +11,12 @@
 #include <unistd.h>
 #include <signal.h>
 #include <sys/epoll.h>
+#if defined(ANDROID)
+#include <fcntl.h>
+#include <asm/unistd.h>
+#else /* GENERIC */
 #include <sys/signalfd.h>
+#endif
 
 #include "hev-event-source-signal.h"
 
@@ -25,6 +30,31 @@ struct _HevEventSourceSignal
 	int signal_fd;
 };
 
+#if defined(ANDROID)
+#define SFD_NONBLOCK	O_NONBLOCK
+
+struct signalfd_siginfo
+{
+  uint32_t ssi_signo;
+  int32_t ssi_errno;
+  int32_t ssi_code;
+  uint32_t ssi_pid;
+  uint32_t ssi_uid;
+  int32_t ssi_fd;
+  uint32_t ssi_tid;
+  uint32_t ssi_band;
+  uint32_t ssi_overrun;
+  uint32_t ssi_trapno;
+  int32_t ssi_status;
+  int32_t ssi_int;
+  uint64_t ssi_ptr;
+  uint64_t ssi_utime;
+  uint64_t ssi_stime;
+  uint64_t ssi_addr;
+  uint8_t __pad[48];
+};
+#endif
+
 static HevEventSourceFuncs hev_event_source_signal_funcs =
 {
 	.prepare = NULL,
@@ -33,6 +63,14 @@ static HevEventSourceFuncs hev_event_source_signal_funcs =
 	.finalize = hev_event_source_signal_finalize,
 };
 
+#if defined(ANDROID)
+static int
+signalfd (int fd, const sigset_t *mask, int flags)
+{
+	return syscall (__NR_signalfd4, fd, mask, _NSIG / 8, flags);
+}
+#endif
+
 HevEventSource *
 hev_event_source_signal_new (int signal)
 {
diff --git a/src/hev-event-source-timeout.c b/src/hev-event-source-timeout.c
index 57a9b00..cacd196 100644
--- a/src/hev-event-source-timeout.c
+++ b/src/hev-event-source-timeout.c
@@ -11,10 +11,20 @@
 #include <errno.h>
 #include <unistd.h>
 #include <sys/epoll.h>
+#if defined(ANDROID)
+#include <fcntl.h>
+#include <asm/unistd.h>
+#else /* GENERIC */
 #include <sys/timerfd.h>
+#endif
 
 #include "hev-event-source-timeout.h"
 
+#if defined(ANDROID)
+#define TFD_NONBLOCK	O_NONBLOCK
+#define TFD_TIMER_ABSTIME 1
+#endif
+
 static bool hev_event_source_timeout_prepare (HevEventSource *source);
 static bool hev_event_source_timeout_check (HevEventSource *source, HevEventSourceFD *fd);
 static void hev_event_source_timeout_finalize (HevEventSource *source);
@@ -35,6 +45,22 @@ static HevEventSourceFuncs hev_event_source_timeout_funcs =
 	.finalize = hev_event_source_timeout_finalize,
 };
 
+#if defined(ANDROID)
+static int
+timerfd_create (int clockid, int flags)
+{
+	return syscall (__NR_timerfd_create, clockid, flags);
+}
+
+static int
+timerfd_settime (int fd, int flags,
+			const struct itimerspec *new_value,
+			struct itimerspec *old_value)
+{
+	return syscall (__NR_timerfd_settime, fd, flags, new_value, old_value);
+}
+#endif
+
 HevEventSource *
 hev_event_source_timeout_new (unsigned int interval)
 {

Over!

重新实现了 Socsk5

可能你会想实现那么多的 Socsk5 能吃吗?其实我只是想研究一下 I/O 模型,拿它来练习一下,如果效果不错也可以拿来用用。本次的实现没有使用 GLib,或许我真的想再也不用 GLib 啦?应该是这样的吧,至少我已经开始在实现我需要的东西。HevLib 就是我的一个初级替代品吧,目前仅仅包含最基本的数据结构、EventSource、EventLoop 等等。更多的数据结构与算法还有类型和对象系统就在以后有需要的时候再实现,一步一步来吧。

重写的 Socks5 Proxy 就是使用了 HevLib 的事件循环机制(Epoll,优先级排序),目前是单线程异步事件驱动的,感觉异步事件驱动实现起来还是有点麻烦的,和内核的进程调度一样,要上下文切换,实际来说 Session 中已经存储了上下文了,要做说也就是流程控制,我是简单的使用了一个状态变量控制的(可能不是太好,有好的方法可以告诉我哦)。

另外,在 DNS 解析上遇到一点麻烦、gethostbyname 虽然工作的没问题,显然这个阻塞的方法并不适合我现在的模型,又看了一下 getaddrinfo_a 和 c-ares 这个异步 DNS 查询库,结果发现想套到当前的事件循环机制中都有一些麻烦。后来就直接基于 UDP 实现了一个简单的异步的标准A记录查询器,简单又好用。

现在服务已经在 VPS 上部署了,RAM 的使用由原来的 300+M 直接下降到 80+M,功能还和原来一样的。;)

代码仓库:https://gitcafe.com/heiher/hev-socks5-proxy

Over!

静态链接 HevSCGIHandler

在原来 HevSCGIServer 的设计中 HevSCGIHandler 接口的实现的整合形式为共享库,HevSCGIServerLibrary 会读取 modules.conf 配置文件,解析出 Handlers 配置,应用于 TaskDispatcher 中。最近由于一个特殊的需求,HevSCGIServerLibrary 和 HevSCGIHandler 都需要静态链接到主程序中,这在原来的设计中上不支持的。

分析发现 HevSCGIHandler 要做的就是定义一个类,主要是要实现了 HevSCGIHandler 接口,模块还实现一个导出函数 hev_scgi_handler_module_get_handler_type 注册与导出 GType。模块被 Library 加载的过程就是通过查找模块的 hev_scgi_handler_module_get_handler_type 函数,调用并得到 GType,然后实例化进增加到 TaskDispatcher。

在此基础上需要对 Library 和 Handler 实现做一些简单的修改就可以支持静态链接 Handler 实现了,首先对于 Library 要做的就是在实例化之前调用 Handler 实现的注册方法向 GType system 注册 GType,然后 Library 根据 modules.conf 中的 TypeName 从 GType system 找到 GType 再进行实例化,最后加入 TaskDispatcher;而对于 Handler 实现来说,就是要简单修改 G_DEFINE_TYPE_DYNAMIC… 为 G_DEFINE_TYPE,在 Makefile 中通过开关宏分离 static 与 shared 的实现。

diff --git a/src/hev-scgi-server.c b/src/hev-scgi-server.c
index ab2d9bb..99f0a40 100644
--- a/src/hev-scgi-server.c
+++ b/src/hev-scgi-server.c
@@ -20,6 +20,9 @@
 #include "hev-scgi-handler-module.h"
 #include "hev-scgi-handler-default.h"
 
+#include <hev-scgi-handler-cgi.h>
+#include <hev-scgi-handler-filebox.h>
+
 enum
 {
        PROP_0,
@@ -148,7 +151,8 @@ static void hev_scgi_server_constructed(GObject * obj)
          g_critical("%s:%d[%s]", __FILE__, __LINE__, __FUNCTION__);
 
        /* Register static handler types at here*/
-
+       HEV_REGISTER_TYPE_SCGI_HANDLER_CGI();
+       HEV_REGISTER_TYPE_SCGI_HANDLER_FILEBOX();
 
        module_dir_path = hev_scgi_config_get_module_dir_path(
                                HEV_SCGI_CONFIG(priv->scgi_config));
# modules.conf
 
[Module0]
Alias=HevSCGIHandlerFilebox
Pattern=^/fb(.*)$
TypeName=HevSCGIHandlerFilebox
 
BaseURI=/fb/
CleanInterval=600
FilePoolPath=/tmp
FileMetaPath=/tmp
FileTempPath=/tmp
 
[Module1]
Alias=HevSCGIHandlerCGI
Pattern=^/cgi((/|\\?)(.*))?$
TypeName=HevSCGIHandlerCGI
 
CGIBinPath=/tmp/test.cgi
WorkDir=/tmp

最后,只要在主程序中将 library 与 handler 实现全部静态链接就可以了。

HevSocks5Service – 简单的 Socks5 服务

HevSocks5Service 是基于 GLib/GObject/GIO 的简单 Socks5 服务实现,参考 RFC1928。初步不支持任何认证且仅支持 CONNECT 命令,采用异步+多线程模式。

在 HevSocks5Service 的 Splice 实现中仍然没有直接使用 GIOStream 的 splice_async 方法,原因和之前在 HevTLSTunnel 中遇到的问题一样,GIO 库中 GTask 的线程池最大并发线程数被硬编码限制为了 10 个,从而使异步方法的并发数限制在一定的值以下。虽然不知道这样设计的目的何在,但这已经影响了对相关的异步方法的调用。

另外,通过对比使用 GIO 和直接使用系统原生 API,感觉基于 GObject 的实现对资源的使用相比较多,库的层层抽象及设计的不合理也使得实现不灵活,或也只能不按套路走。是不是我应该不要再过多的“依赖” GLib 及相关的库了?

Over!