从pthread_self看GNU ld链接器

时间:2022-07-14 17:35:04

一、问题引出

对于主线程(也就是main函数对应的线程),它并不是通过pthread_create创建的线程,所以我们没有这个主线程对应的pthread_t结构,这个结构也就是pthread_create的第一个参数。这当然只是最为直观的一个结论,事实上系统不会这么羸弱,在main函数中通过pthread_self函数就可以获得主线程的线程描述符。为什么呢?因为对于一个函数来说,它并不知道也不应该知道自己是否是被主线程调用,不可能每个函数都要判断自己是被主线程调用还是其它线程调用。

二、pthread库实现

1、__pthread_initialize_minimal_internal

从C库是先看,pthread库中很多基本的操作都是在这个函数中实现的,而这个函数就是坐落在这个init.c文件中,但是单单从这个文件来看,并不能看出这个文件有什么特殊之处,至少看不出来它会在系统启动的时候被执行以至于pthread_self可以有点意义。现在我们先分析一下这个函数是如何让pthread_self有意义的,然后再说明它是如何被调用的。初始化的调用连:

__pthread_initialize_minimal_internal--->>> __libc_setup_tls (TLS_TCB_SIZE, TLS_TCB_ALIGN)--->>> TLS_INIT_TP (tlsblock, 0):

  /* Install the TLS.  */            \
     asm volatile (TLS_LOAD_EBX            \
     "int $0x80\n\t"           \
     TLS_LOAD_EBX            \
     : "=a" (_result), "=m" (_segdescr.desc.entry_number)       \
     : "0" (__NR_set_thread_area),         \
       TLS_EBX_ARG (&_segdescr.desc), "m" (_segdescr.desc));    \

如果你很好奇这个线程描述符是在哪里分配的,也就是它的逻辑位置在哪里?那么从__libc_setup_tls函数可以找到答案,在这个函数中__sbrk扩充了主线程的堆栈大小,也就是申请了一个pthread_t结构和对应的tls数据区,从而让主线程的“堆栈+TLS"也和其它pthread线程一致,从而保证pthread库中接口的一致性。

2、__pthread_initialize_minimal被调用的时机

简单搜索这个函数被调用的地方,可以发现glibc-2.7\nptl\sysdeps\pthread\pt-initfini.c中有对这个函数的调用,而且这个文件的名字给出的提示是这个文件是PThread的初始化和结束化相关操作。

由于其内容少儿重要,所以这里进行摘抄:

/* The beginning of _init:  */
asm ("\n/*@_init_PROLOG_BEGINS*/");注意,这个将是下面Makefile脚本中sed处理定界符的来源

static void
call_initialize_minimal (void)
{
  extern void __pthread_initialize_minimal_internal (void)
    __attribute ((visibility ("hidden")));

  __pthread_initialize_minimal_internal ();
}

SECTION (".init");
extern void __attribute__ ((section (".init"))) _init (void);
void
_init (void)
{
  /* The very first thing we must do is to set up the registers.  */
  call_initialize_minimal ();

  asm ("ALIGN");
  asm("END_INIT");
  /* Now the epilog. */
  asm ("\n/*@_init_PROLOG_ENDS*/");注意,这个将是下面Makefile脚本中sed处理定界符的来源

也就是在_init函数中调用了这个__pthread_initialize_minimal函数,从而完成了主线程的初始化。但是如果搜索对这个_init的调用,那可能会发现很多莫名其妙的调用地方,莫名其妙到无法对应。而且明显地,glibc-2.7\sysdeps\generic\initfini.c中定义的_init函数更有竞争力,因为编译过glibc的同学都知道,pthread库是glibc的一个addons选项,也就是一个“添头”,所以如果libc和pthread库一起链接,一定使用的是generic中的_init函数。说了这么多,可能让问题更加扑朔迷离了,以为我自己也没有说清楚(但是是想清楚的),所以直接正向的描述这个问题吧。

在 glibc-2.7\nptl\Makefile中有下面的代码

(objpfx)pt-initfini.spt-initfini.c
 $(compile.c) -S $(CFLAGS-pt-initfini.s) -finhibit-size-directive \
  $(patsubst -f%,-fno-%,$(exceptions)) -o $@

………………

# We only have one kind of startup code files.  Static binaries and
# shared libraries are build using the PIC version.
$(objpfx)crti.S: $(objpfx)pt-initfini.s
 sed -n -e '1,/@HEADER_ENDS/p' \
        -e '/@_.*_PROLOG_BEGINS/,/@_.*_PROLOG_ENDS/p' \这个sed表达式的意思就是将@_.*_PROLOG_BEGINS到@_.*_PROLOG_ENDS之间的内容输出到标准输出中,由于接下来将这个标准输出重定向到了$@(也就是crti.s中),加上前面看到pt-initfini.s是由pt-initfini.c生成的,所以,结合pt-initfini.c中的_init函数将会被输出到这个crti.s文件中,加上这个函数通过extern void __attribute__ ((section (".init"))) _init (void);声明要求将自己安葬在.init节中,所以在crti.s中,这个函数依然是在.init节中

        -e '/@TRAILER_BEGINS/,$$p' $< > $@
$(objpfx)crtn.S: $(objpfx)pt-initfini.s
 sed -n -e '1,/@HEADER_ENDS/p' \
        -e '/@_.*_EPILOG_BEGINS/,/@_.*_EPILOG_ENDS/p' \
        -e '/@TRAILER_BEGINS/,$$p' $< > $@

$(objpfx)defs.h: $(objpfx)pt-initfini.s
 sed -n -e '/@TESTS_BEGIN/,/@TESTS_END/p' $< | \
  $(AWK) -f ../csu/defs.awk > $@
3、_init函数被调用的时机

\glibc-2.7\sysdeps\i386\elf\start.S

 /* Push address of our own entry points to .fini and .init.  */
 pushl $__libc_csu_fini
 pushl $__libc_csu_init

 pushl %ecx  /* Push second argument: argv.  */
 pushl %esi  /* Push first argument: argc.  */

 pushl $BP_SYM (main)

 /* Call the user's main function, and exit with its value.
    But let the libc call main.    */
 call BP_SYM (__libc_start_main)

\glibc-2.7\csu\elf-init.c

void
__libc_csu_init (int argc, char **argv, char **envp)
{
  /* For dynamically linked executables the preinit array is executed by
     the dynamic linker (before initializing any shared object.  */

#ifndef LIBC_NONSHARED
  /* For static executables, preinit happens rights before init.  */
  {
    const size_t size = __preinit_array_end - __preinit_array_start;
    size_t i;
    for (i = 0; i < size; i++)
      (*__preinit_array_start [i]) (argc, argv, envp);
  }
#endif

  _init ();

  const size_t size = __init_array_end - __init_array_start;
  for (size_t i = 0; i < size; i++)
      (*__init_array_start [i]) (argc, argv, envp);
}

SECTION (".init");
extern void __attribute__ ((section (".init"))) _init (void);
void
_init (void)
{
  /* We cannot use the normal constructor mechanism in gcrt1.o because it
     appears before crtbegin.o in the link, so the header elt of .ctors
     would come after the elt for __gmon_start__.  One approach is for
     gcrt1.o to reference a symbol which would be defined by some library
     module which has a constructor; but then user code's constructors
     would come first, and not be profiled.  */
  call_gmon_start ();

  asm ("ALIGN");
  asm("END_INIT");
  /* Now the epilog. */
  asm ("\n/*@_init_PROLOG_ENDS*/");
  asm ("\n/*@_init_EPILOG_BEGINS*/");
  SECTION(".init");
}
asm ("END_INIT");