linux:内核如何定位并调用设备驱动初始化函数

作者:jafon.tian

转载请注明出处:https://blog.csdn.net/JT_Notes

写过linux驱动程序的人都知道需要将驱动的初始化函数通过module_init注册,然后在通过menuconfig配置的时候选择随内核一起编译(非模块),系统在启动的时候就能够自动调用驱动初始化函数了。真是一件神奇的事情!

驱动程序模板(模板来源:https://blog.csdn.net/zhuhuibeishadiao/article/details/51407438)

#include 
#include 
MODULE_LICENSE ("GPL");//开源协议GPL 或者Dual BSD
MODULE_AUTHOR ("TOM");//作者
MODULE_DESCRIPTION ("MY_TEST");//描述此驱动
//EXPORT_NO_SYMBOLS;//不导出函数 可以不写
//EXPORT_SYMBOL(hello_data);//导出hello_data
int test_init(void)
{
    printk(KERN_INFO "hello world\n");
    return 0;
}
void test_exit(void)
{
   printk(KERN_INFO "goodbye world\n");
}
module_init(test_init); //注册DriverEntry	 
module_exit(test_exit); //注册DriverUnload

内核是如何知道存在这个驱动?又是在何时调用了驱动初始化函数?看来秘密应该就在这个module_init上。我们就从module_init入手,在linux kernel的源码中找寻这个问题的答案。下面是对相关代码的摘录,省略了不相关的部分。(文件位于include/linux/init.h)

/*
 * Used for initialization calls..
 */
typedef int (*initcall_t)(void);

/* initcalls are now grouped by functionality into separate 
 * subsections. Ordering inside the subsections is determined
 * by link order. 
 * For backwards compatibility, initcall() puts the call in 
 * the device init subsection.
 *
 * The `id' arg to __define_initcall() is needed so that multiple initcalls
 * can point at the same handler without causing duplicate-symbol build errors.
 */

#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id __used \
	__attribute__((__section__(".initcall" #id ".init"))) = fn

#define device_initcall(fn)		__define_initcall(fn, 6)

#define __initcall(fn) device_initcall(fn)
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 * 
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)	__initcall(x);

从代码我们可以看到module_init是一个嵌套宏定义,嵌套层次为:
module_init->__initcall->device_initcall->__define_initcall

经过这么多层嵌套,模板中的module_init(test_init)到底最后变成了什么模样?我们做个实验,首先对模板程序进行适当的调整,并命名为driver.c。

// driver.c
typedef int (*initcall_t)(void);
#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id __used \
	__attribute__((__section__(".initcall" #id ".init"))) = fn

#define device_initcall(fn)		__define_initcall(fn, 6)

#define __initcall(fn) device_initcall(fn)

#define module_init(x)	__initcall(x);

int test_init(void)
{
    return 0;
}

module_init(test_init); 

接下在我们使用gcc对driver.c进行预处理

$ gcc -E driver.c 
# 1 "driver.c"
# 1 ""
# 1 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "" 2
# 1 "driver.c"
typedef int (*initcall_t)(void);
# 12 "driver.c"
int test_init(void)
{
    return 0;
}

static initcall_t __initcall_test_init6 __used __attribute__((__section__(".initcall" "6" ".init"))) = test_init;;

module_init(test_init);经过几次宏扩展变成了static initcall_t __initcall_test_init6 __used attribute((section(".initcall" “6” “.init”))) = test_init;看上去是变成了一个变量赋值语句,秘密一定就在这条语句中,我们逐个来分析一下它的组成部分

序号 组成部分 说明
1 static 关键字,说明是文件内部变量
2 initcall_t 自定义函数指针,见driver.c开头部分
3 __initcall_test_init6 变量名,看着和test_init函数还是有些儿像的哦
4 __used 变量的编译器属性,这个和本次实验关系不大,有兴趣的可以去内核源码中搜索一下
5 attribute((section(".initcall" “6” “.init”))) 变量的编译器属性,这个是秘密的重点!!!
6 test_init 初始化函数名

我们着重看一下第5项,对于编译器而言这一项相当于__attribute__((section(".initcall6.init"))),可这又是什么意思呢?就是告诉编译器要把这个变量放到段名是".initcall6.init"的段里。对于什么叫做段(section),大家可以自行补习一下编译原理和ELF文件结构。简单着理解,想象一下程序是由许多段组成的,有些儿段里放的是程序,有些儿段里放的是数据。这句话就是将变量__initcall_test_init6放到.initcall6.init数据段里。

这样就行了吗?就放到.initcall6.init段里内核就知道需要调用啦?事实确实是这样子的,那内核又是怎么调用的呢?其实这才是内核设计的巧妙所在,我们来看一下内核的启动文件/init/main.c。关键部分摘录如下。

extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];

static initcall_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
	"early",
	"core",
	"postcore",
	"arch",
	"subsys",
	"fs",
	"device",
	"late",
};

static void __init do_initcall_level(int level)
{
	extern const struct kernel_param __start___param[], __stop___param[];
	initcall_t *fn;

	strcpy(static_command_line, saved_command_line);
	parse_args(initcall_level_names[level],
		   static_command_line, __start___param,
		   __stop___param - __start___param,
		   level, level,
		   &repair_env_string);

	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(*fn);
}

static void __init do_initcalls(void)
{
	int level;

	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);
}

内核对初始化调用分为了8个等级"early"、“core”、“postcore”、“arch”、“subsys”、“fs”、“device”、“late”,每个等级又对应着一个类型为initcall_t *的变量,依次为__initcall0_start、__initcall1_start、__initcall2_start、 __initcall3_start、__initcall4_start、__initcall5_start、__initcall6_start、__initcall7_start。

这里我们就看到__initcall6_start和之前的段.initcall6.init应该有些联系。可是我们搜遍内核代码都找不到__initcall6_start变量的定义。那__initcall6_start究竟在哪里?

我们来看/arch/XXX/kernel/vmlinux.lds文件(其中XXX是平台名字,如果是PC的话就是x86,我用的是mips平台),这个文件是自动生成的,需要配置和编译内核源码。那这个lds是什么文件呢?lds就是链接脚本文件,我们知道程序是需要通过编译然后链接,最后才能生成完整的可执行程序,内核也不例外。只是我们平时用gcc编译应用程序的时候,都是使用的内置默认lds文件,但是内核是提供了自身的链接脚本文件的。对链接脚本文件语法有兴趣的参考https://sourceware.org/binutils/docs/ld/index.html#SEC_Contents

摘录关键部分如下

 /* will be freed after init */
 . = ALIGN(4096); /* Init code and data */
 __init_begin = .;
 . = ALIGN(4096); 
 .init.text : AT(ADDR(.init.text) - 0) 
 { 
     _sinittext = .; 
     *(.init.text) *(.cpuinit.text) *(.meminit.text) _einittext = .; 
 }
 .init.data : AT(ADDR(.init.data) - 0) 
 { 
     *(.init.data) *(.cpuinit.data) *(.meminit.data) *(.init.rodata) *(.cpuinit.rodata) *(.meminit.rodata) . = ALIGN(32); 
     __dtb_start = .; 
     *(.dtb.init.rodata) __dtb_end = .; 
     . = ALIGN(16); 
     __setup_start = .; 
     *(.init.setup) __setup_end = .; 
     __initcall_start = .; 
     *(.initcallearly.init) 
     __initcall0_start = .; 
     *(.initcall0.init) *(.initcall0s.init) 
     __initcall1_start = .; 
     *(.initcall1.init) *(.initcall1s.init) 
     __initcall2_start = .; 
     *(.initcall2.init) *(.initcall2s.init) 
     __initcall3_start = .; 
     *(.initcall3.init) *(.initcall3s.init) 
     __initcall4_start = .; 
     *(.initcall4.init) *(.initcall4s.init) 
     __initcall5_start = .; 
     *(.initcall5.init) *(.initcall5s.init) 
     __initcallrootfs_start = .; 
     *(.initcallrootfs.init) *(.initcallrootfss.init) 
     __initcall6_start = .; 
     *(.initcall6.init) *(.initcall6s.init) 
     __initcall7_start = .; 
     *(.initcall7.init) *(.initcall7s.init) 
     __initcall_end = .; 
     __con_initcall_start = .; 
     *(.con_initcall.init) 
     __con_initcall_end = .; 
     __security_initcall_start = .; 
     *(.security_initcall.init)
     __security_initcall_end = .; }

总算是找到 __initcall6_start了,原来它藏在vmlinux.lds里,可这是什么意思呢?

语句 释义
__initcall6_start = .; 这句是将当前的链接地址赋值给__initcall6_start
*(.initcall6.init) 为.initcall6.init申请接下来的链接地址空间

也就是说__initcall6_start拥有.initcall6.init的起始地址,所有指定分配到.initcall6.init段的变量都将在这个地址之后顺序排列。结合do_initcall_level的代码来看一下。fn是指向initcall_t类型的指针,我们看那个for循环,当level=6的时候,fn赋值为initcall_levels[6],也就是__initcall6_start,也就是分配到.initcall6.init段里的第一个initcall_t类型变量,假如只有模板中的那一个驱动的话,也就是指向了__initcall_test_init6,通过*fn就自然取到了test_init。

static void __init do_initcall_level(int level)
{
	extern const struct kernel_param __start___param[], __stop___param[];
	initcall_t *fn;

	strcpy(static_command_line, saved_command_line);
	parse_args(initcall_level_names[level],
		   static_command_line, __start___param,
		   __stop___param - __start___param,
		   level, level,
		   &repair_env_string);

	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(*fn);
}

写到这里,相信大家就明白了,原来内核是通过指定段存储函数指针结合链接文件来实现的定位并调用设备驱动初始化函数。

下面使用一个例子,在应用层来演示一下这个技术实现。我们需要编写c和lds文件

/*
 * Copyright (C) 2018  Jafon.Tian
 */
#include 

typedef int (*initcall_t)(void);
#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id \
	__attribute__((__section__(".initcall" #id ".init"))) = fn

#define device_initcall(fn)		__define_initcall(fn, 6)

#define __initcall(fn) device_initcall(fn)

#define module_init(x)	__initcall(x);

extern initcall_t __initcall6_start[];
extern initcall_t __initcall6_end[];

int test_init_1(void)
{
    fprintf(stdout, "In test_init_1 ...\n");
    return 0;
}

int test_init_2(void)
{
    fprintf(stdout, "In test_init_2 ...\n");
    return 0;
}

module_init(test_init_1);
module_init(test_init_2);

void do_initcall_level6(void)
{
    initcall_t *fn = __initcall6_start;
    for(fn = __initcall6_start; fn < __initcall6_end; fn++)
        (*fn)();
    return;
}

int main(int argc, char* argv[])
{
    fprintf(stdout, "start testing\n");
    do_initcall_level6();
    fprintf(stdout, "end testing\n");
    return 0;
}

lds文件如下(可以通过ld --verbose命令得到内置的链接脚本,在此基础上进行修改)

/* linker2.lds */
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2015 Free Software Foundation, Inc.
   Copying and distribution of this script, with or without modification,
   are permitted in any medium without royalty provided the copyright
   notice and this notice are preserved.  */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
              "elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
SECTIONS
{
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
  .interp         : { *(.interp) }
  .note.gnu.build-id : { *(.note.gnu.build-id) }
  .hash           : { *(.hash) }
  .gnu.hash       : { *(.gnu.hash) }
  .dynsym         : { *(.dynsym) }
  .dynstr         : { *(.dynstr) }
  .gnu.version    : { *(.gnu.version) }
  .gnu.version_d  : { *(.gnu.version_d) }
  .gnu.version_r  : { *(.gnu.version_r) }
  .rela.dyn       :
    {
      *(.rela.init)
      *(.rela.text .rela.text.* .rela.gnu.linkonce.t.*)
      *(.rela.fini)
      *(.rela.rodata .rela.rodata.* .rela.gnu.linkonce.r.*)
      *(.rela.data .rela.data.* .rela.gnu.linkonce.d.*)
      *(.rela.tdata .rela.tdata.* .rela.gnu.linkonce.td.*)
      *(.rela.tbss .rela.tbss.* .rela.gnu.linkonce.tb.*)
      *(.rela.ctors)
      *(.rela.dtors)
      *(.rela.got)
      *(.rela.bss .rela.bss.* .rela.gnu.linkonce.b.*)
      *(.rela.ldata .rela.ldata.* .rela.gnu.linkonce.l.*)
      *(.rela.lbss .rela.lbss.* .rela.gnu.linkonce.lb.*)
      *(.rela.lrodata .rela.lrodata.* .rela.gnu.linkonce.lr.*)
      *(.rela.ifunc)
    }
  .rela.plt       :
    {
      *(.rela.plt)
      PROVIDE_HIDDEN (__rela_iplt_start = .);
      *(.rela.iplt)
      PROVIDE_HIDDEN (__rela_iplt_end = .);
    }
  .init           :
  {
    KEEP (*(SORT_NONE(.init)))
  }
  .plt            : { *(.plt) *(.iplt) }
.plt.got        : { *(.plt.got) }
.plt.bnd        : { *(.plt.bnd) }
  .text           :
  {
    *(.text.unlikely .text.*_unlikely .text.unlikely.*)
    *(.text.exit .text.exit.*)
    *(.text.startup .text.startup.*)
    *(.text.hot .text.hot.*)
    *(.text .stub .text.* .gnu.linkonce.t.*)
    /* .gnu.warning sections are handled specially by elf32.em.  */
    *(.gnu.warning)
  }
  .fini           :
  {
    KEEP (*(SORT_NONE(.fini)))
  }
  PROVIDE (__etext = .);
  PROVIDE (_etext = .);
  PROVIDE (etext = .);
  .rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
  .rodata1        : { *(.rodata1) }
  .eh_frame_hdr : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
  .eh_frame       : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) }
  .gcc_except_table   : ONLY_IF_RO { *(.gcc_except_table
  .gcc_except_table.*) }
  .gnu_extab   : ONLY_IF_RO { *(.gnu_extab*) }
  /* These sections are generated by the Sun/Oracle C++ compiler.  */
  .exception_ranges   : ONLY_IF_RO { *(.exception_ranges
  .exception_ranges*) }
  /* Adjust the address for the data segment.  We want to adjust up to
     the same address within the page on the next page up.  */
  . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
  /* Exception handling  */
  .eh_frame       : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) }
  .gnu_extab      : ONLY_IF_RW { *(.gnu_extab) }
  .gcc_except_table   : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) }
  .exception_ranges   : ONLY_IF_RW { *(.exception_ranges .exception_ranges*) }
  /* Thread Local Storage sections  */
  .tdata          : { *(.tdata .tdata.* .gnu.linkonce.td.*) }
  .tbss           : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
  .preinit_array     :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  }
  .init_array     :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
    KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
    PROVIDE_HIDDEN (__init_array_end = .);
  }
  .fini_array     :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
    KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
    PROVIDE_HIDDEN (__fini_array_end = .);
  }
  .ctors          :
  {
    /* gcc uses crtbegin.o to find the start of
       the constructors, so we make sure it is
       first.  Because this is a wildcard, it
       doesn't matter if the user does not
       actually link against crtbegin.o; the
       linker won't look for a file to match a
       wildcard.  The wildcard also means that it
       doesn't matter which directory crtbegin.o
       is in.  */
    KEEP (*crtbegin.o(.ctors))
    KEEP (*crtbegin?.o(.ctors))
    /* We don't want to include the .ctor section from
       the crtend.o file until after the sorted ctors.
       The .ctor section from the crtend file contains the
       end of ctors marker and it must be last */
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
    KEEP (*(SORT(.ctors.*)))
    KEEP (*(.ctors))
  }
  .dtors          :
  {
    KEEP (*crtbegin.o(.dtors))
    KEEP (*crtbegin?.o(.dtors))
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
    KEEP (*(SORT(.dtors.*)))
    KEEP (*(.dtors))
  }
  .jcr            : { KEEP (*(.jcr)) }
  .data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) }
  .dynamic        : { *(.dynamic) }
  .got            : { *(.got) *(.igot) }
  . = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);
  .got.plt        : { *(.got.plt)  *(.igot.plt) }
  .data           :
  {
    *(.data .data.* .gnu.linkonce.d.*)
    SORT(CONSTRUCTORS)
  }
  .data1          : { *(.data1) }
  _edata = .; PROVIDE (edata = .);
  . = .;
  __bss_start = .;
  .bss            :
  {
   *(.dynbss)
   *(.bss .bss.* .gnu.linkonce.b.*)
   *(COMMON)
   /* Align here to ensure that the .bss section occupies space up to
      _end.  Align after .bss to ensure correct alignment even if the
      .bss section disappears because there are no input sections.
      FIXME: Why do we need it? When there is no .bss section, we don't
      pad the .data section.  */
   . = ALIGN(. != 0 ? 64 / 8 : 1);
  }
  .lbss   :
  {
    *(.dynlbss)
    *(.lbss .lbss.* .gnu.linkonce.lb.*)
    *(LARGE_COMMON)
  }
  . = ALIGN(64 / 8);
  . = SEGMENT_START("ldata-segment", .);
  .lrodata   ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
  {
    *(.lrodata .lrodata.* .gnu.linkonce.lr.*)
  }
  .ldata   ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
  {
    *(.ldata .ldata.* .gnu.linkonce.l.*)
    . = ALIGN(. != 0 ? 64 / 8 : 1);
  }
  . = ALIGN(64 / 8);
  _end = .; PROVIDE (end = .);
  . = DATA_SEGMENT_END (.);
  . = SEGMENT_START(".initcall6.init", .);
  __initcall6_start = ADDR(.initcall6.init);
  .initcall6.init   : { *(.initcall6.init) }
  __initcall6_end = .;
  /* Stabs debugging sections.  */
  .stab          0 : { *(.stab) }
  .stabstr       0 : { *(.stabstr) }
  .stab.excl     0 : { *(.stab.excl) }
  .stab.exclstr  0 : { *(.stab.exclstr) }
  .stab.index    0 : { *(.stab.index) }
  .stab.indexstr 0 : { *(.stab.indexstr) }
  .comment       0 : { *(.comment) }
  /* DWARF debug sections.
     Symbols in the DWARF debugging sections are relative to the beginning
     of the section so we begin them at 0.  */
  /* DWARF 1 */
  .debug          0 : { *(.debug) }
  .line           0 : { *(.line) }
  /* GNU DWARF 1 extensions */
  .debug_srcinfo  0 : { *(.debug_srcinfo) }
  .debug_sfnames  0 : { *(.debug_sfnames) }
  /* DWARF 1.1 and DWARF 2 */
  .debug_aranges  0 : { *(.debug_aranges) }
  .debug_pubnames 0 : { *(.debug_pubnames) }
  /* DWARF 2 */
  .debug_info     0 : { *(.debug_info .gnu.linkonce.wi.*) }
  .debug_abbrev   0 : { *(.debug_abbrev) }
  .debug_line     0 : { *(.debug_line .debug_line.* .debug_line_end ) }
  .debug_frame    0 : { *(.debug_frame) }
  .debug_str      0 : { *(.debug_str) }
  .debug_loc      0 : { *(.debug_loc) }
  .debug_macinfo  0 : { *(.debug_macinfo) }
  /* SGI/MIPS DWARF 2 extensions */
  .debug_weaknames 0 : { *(.debug_weaknames) }
  .debug_funcnames 0 : { *(.debug_funcnames) }
  .debug_typenames 0 : { *(.debug_typenames) }
  .debug_varnames  0 : { *(.debug_varnames) }
  /* DWARF 3 */
  .debug_pubtypes 0 : { *(.debug_pubtypes) }
  .debug_ranges   0 : { *(.debug_ranges) }
  /* DWARF Extension.  */
  .debug_macro    0 : { *(.debug_macro) }
  .gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
  /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}

进行编译

$ gcc -Tlinker2.lds l2.c -o l2

测试

$ ./l2
start testing
In test_init_1 ...
In test_init_2 ...
end testing

你可能感兴趣的:(小玩意)