LD_PRELOAD-ing go 可执行文件时未调用共享对象中的构造函数
Constructor in shared object not called in when LD_PRELOAD-ing a go executable
在 Alpine 映像中内置的 GO 可执行文件有一个奇怪的行为,其中标准 LD_PRELOAD 功能无法正常工作。
看起来构造函数没有被动态加载器调用!
我有一个示例 go 应用程序 (getgoogle.go
):
package main
import (
"fmt"
"net/http"
)
func main() {
resp, err := http.Get("http://google.com/")
if err == nil {
fmt.Println(resp.StatusCode)
}
}
以及示例共享对象代码 (libldptest.c
)
#include <stdio.h>
static void __attribute__((constructor)) StaticConstructor(int argc, char **argv, char **env)
{
printf(">>> LD_PRELOADED!\n");
}
我正在使用这个 Dockerfile(gotest
图像)创建基于 debian 的 docker 图像:
FROM golang
COPY libldptest.c hello-world.go /
RUN gcc -shared -o /libldptest.so /libldptest.c
RUN go build -gcflags='-N -l' -o /getgoogle /getgoogle.go
ENV LD_PRELOAD=/libldptest.so
然后运行以下命令:
$docker run -it gotest /getgoogle
>>> LD_PRELOADED!
200
这意味着构造函数在这里工作。
但是当对基于高山的 docker 图像执行相同操作时
FROM golang:1.12-alpine
RUN apk add gcc libc-dev
COPY libldptest.c hello-world.go /
RUN gcc -shared -o /libldptest.so /libldptest.c
RUN go build -gcflags='-N -l' -o /getgoogle /getgoogle.go
ENV LD_PRELOAD=/libldptest.so
和运行与上面相同的命令
$docker run -it gotest /getgoogle
200
$docker run -it gotest ls
>>> LD_PRELOADED!
bin src
意思是当运行 go 应用程序时静态构造函数没有被调用! (但在 运行 ls
时被调用)
请注意,我已检查动态加载程序是否将库添加到进程 space。
如果能了解它为何不起作用,我将不胜感激。
停止忽略第一条评论。如果你坚持使用 Go 的内部 linker 而不是 link 以与 libc 兼容的方式使用那么你不能使用任何 C 代码,包括 LD_PRELOAD
ed C 代码甚至动态 linker 本身的功能。正如 Florian(来自 glibc)在 linked 问题中所说,它对 glibc 也无效,"working" 只是偶然发生的。
即使您以某种方式弄清楚 "mechanically" 为什么您的 ctor 没有被调用,您仍然 运行 C 代码处于损坏的进程状态,任何事情都可能出错。即使您分析了所有内容并且看起来不错,这也可能会随着下一次动态 linker/libc 更新而完全改变。
如果您想这样做,请使用 Go 中的外部 linker 选项。
从评论中可以看出,Go/Alpine 环境中的静态构造函数存在一个主要问题。简而言之,从 ABI 的角度来看,调用静态构造函数的要求分配给可执行文件而不是加载程序。 Go 可执行文件不基于 C 运行时,它只调用依赖共享对象的静态构造函数,而不是 LD_PRELOAD
-ed 共享对象。在 glibc 的情况下,LD_PRELOAD
-ed 共享对象的构造函数由实现调用,而不是由加载程序设计调用。在 musl-libc 上它们不是。
我已经制定了 "hack"-ish 解决方法,使现有的 Go 应用程序与 LD_PRELOAD
-ed 共享对象一起工作。我使用的事实是库在这个环境中由 musl-libc 正确地 LD_PRELOAD
-ed 并且 Go 非常调用 pthread_create
初始化的早期阶段。
我是 overriding/hooking LD_PRELOAD
共享对象中的 pthread_create
符号,并用它来调用构造函数。
#include <pthread.h>
#include <dlfcn.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
{
int (*pthread_create_original)(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) = dlsym(RTLD_NEXT,"pthread_create");
static int already_called = 0;
if (!already_called)
{
already_called = 1;
// call here your constructors
}
return pthread_create_original(thread,attr,start_routine,arg);
}
注意事项: 这适用于当前的 Go 运行时,但构建此解决方案的假设远非未来证明。下一个 Go 版本可以轻松打破它。
在 Alpine 映像中内置的 GO 可执行文件有一个奇怪的行为,其中标准 LD_PRELOAD 功能无法正常工作。
看起来构造函数没有被动态加载器调用!
我有一个示例 go 应用程序 (getgoogle.go
):
package main
import (
"fmt"
"net/http"
)
func main() {
resp, err := http.Get("http://google.com/")
if err == nil {
fmt.Println(resp.StatusCode)
}
}
以及示例共享对象代码 (libldptest.c
)
#include <stdio.h>
static void __attribute__((constructor)) StaticConstructor(int argc, char **argv, char **env)
{
printf(">>> LD_PRELOADED!\n");
}
我正在使用这个 Dockerfile(gotest
图像)创建基于 debian 的 docker 图像:
FROM golang
COPY libldptest.c hello-world.go /
RUN gcc -shared -o /libldptest.so /libldptest.c
RUN go build -gcflags='-N -l' -o /getgoogle /getgoogle.go
ENV LD_PRELOAD=/libldptest.so
然后运行以下命令:
$docker run -it gotest /getgoogle
>>> LD_PRELOADED!
200
这意味着构造函数在这里工作。
但是当对基于高山的 docker 图像执行相同操作时
FROM golang:1.12-alpine
RUN apk add gcc libc-dev
COPY libldptest.c hello-world.go /
RUN gcc -shared -o /libldptest.so /libldptest.c
RUN go build -gcflags='-N -l' -o /getgoogle /getgoogle.go
ENV LD_PRELOAD=/libldptest.so
和运行与上面相同的命令
$docker run -it gotest /getgoogle
200
$docker run -it gotest ls
>>> LD_PRELOADED!
bin src
意思是当运行 go 应用程序时静态构造函数没有被调用! (但在 运行 ls
时被调用)
请注意,我已检查动态加载程序是否将库添加到进程 space。
如果能了解它为何不起作用,我将不胜感激。
停止忽略第一条评论。如果你坚持使用 Go 的内部 linker 而不是 link 以与 libc 兼容的方式使用那么你不能使用任何 C 代码,包括 LD_PRELOAD
ed C 代码甚至动态 linker 本身的功能。正如 Florian(来自 glibc)在 linked 问题中所说,它对 glibc 也无效,"working" 只是偶然发生的。
即使您以某种方式弄清楚 "mechanically" 为什么您的 ctor 没有被调用,您仍然 运行 C 代码处于损坏的进程状态,任何事情都可能出错。即使您分析了所有内容并且看起来不错,这也可能会随着下一次动态 linker/libc 更新而完全改变。
如果您想这样做,请使用 Go 中的外部 linker 选项。
从评论中可以看出,Go/Alpine 环境中的静态构造函数存在一个主要问题。简而言之,从 ABI 的角度来看,调用静态构造函数的要求分配给可执行文件而不是加载程序。 Go 可执行文件不基于 C 运行时,它只调用依赖共享对象的静态构造函数,而不是 LD_PRELOAD
-ed 共享对象。在 glibc 的情况下,LD_PRELOAD
-ed 共享对象的构造函数由实现调用,而不是由加载程序设计调用。在 musl-libc 上它们不是。
我已经制定了 "hack"-ish 解决方法,使现有的 Go 应用程序与 LD_PRELOAD
-ed 共享对象一起工作。我使用的事实是库在这个环境中由 musl-libc 正确地 LD_PRELOAD
-ed 并且 Go 非常调用 pthread_create
初始化的早期阶段。
我是 overriding/hooking LD_PRELOAD
共享对象中的 pthread_create
符号,并用它来调用构造函数。
#include <pthread.h>
#include <dlfcn.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
{
int (*pthread_create_original)(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) = dlsym(RTLD_NEXT,"pthread_create");
static int already_called = 0;
if (!already_called)
{
already_called = 1;
// call here your constructors
}
return pthread_create_original(thread,attr,start_routine,arg);
}
注意事项: 这适用于当前的 Go 运行时,但构建此解决方案的假设远非未来证明。下一个 Go 版本可以轻松打破它。