【问题标题】:Constructor in shared object not called in when LD_PRELOAD-ing a go executableLD_PRELOAD-ing go 可执行文件时未调用共享对象中的构造函数
【发布时间】:2020-05-19 07:33:19
【问题描述】:

在 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

这意味着构造函数在这里工作。

但是当对一个基于 alpine 的 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时被调用)

请注意,我已经检查过动态加载器将库添加到进程空间。

如果能理解它为什么不起作用,我将不胜感激。

【问题讨论】:

  • 这解释了问题!
  • 这个我真的没看过,不过好像和问题有关……
  • 需要做的是将我的构造函数添加到__CTOR_LIST__表中,因为它是在没有GO的Alpine loader中调用的。我正在研究如何做到这一点

标签: c go alpine ld-preload


【解决方案1】:

停止忽略第一条评论。如果你坚持使用 Go 的内部链接器,它不以与 libc 使用兼容的方式链接,那么你不能使用任何 C 代码,包括 LD_PRELOADed C 代码甚至动态链接器本身的功能。正如 Florian(来自 glibc)在链接问题中所说,它对 glibc 也无效,并且只是偶然在那里“工作”。

即使您以某种方式“机械地”弄清楚了为什么您的 ctor 没有被调用,您仍然会在损坏的进程状态下运行 C 代码,并且任何事情都可能出错。即使您分析了所有内容并且看起来还不错,但随着下一次动态链接器/libc 更新,这可能会完全改变。

如果您想这样做,请使用 Go 中的外部链接器选项。

【讨论】:

  • 我想我没有阐明我的问题的前提。我正在使用 LD_PRELOAD 因为我无法控制 Go 应用程序,也没有它的链接/编译标志...
  • 那么你就被卡住了,没有破解二进制文件。应用程序的链接方式与使用 LD_PRELOAD 不兼容。
【解决方案2】:

从 cmets 可以看出,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 初始化的早期阶段。

我在LD_PRELOAD-ed 共享对象中覆盖/挂钩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 运行时,但构建此解决方案的假设远非未来证明。 Next Go 版本可以轻松打破它。

【讨论】:

    猜你喜欢
    • 2011-08-04
    • 1970-01-01
    • 1970-01-01
    • 2020-04-26
    • 2015-07-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多