Linux Kernel编译和链接中的linker script语法是怎样的

发布时间:2021-11-26 15:49:34 作者:柒染
来源:亿速云 阅读:128

这篇文章将为大家详细讲解有关Linux Kernel编译和链接中的linker script语法是怎样的,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

先要讲讲这个问题是怎么来的。

我在编译内核的时候,发现arch/arm/kernel目录下有一个这样的文 件:vmlinux.lds.S。第一眼看上去,想想是不是汇编文件呢?打开一看,好像不是。那它是干嘛的?而且前面已经说过,make  V=1的时候,发现这个文件的用处在ld命令中,即ld -T vmlinux.lds.S,好像是链接命令用的,如下所示

如arm-linux-ld -EL -p --no-undefined -X --build-id -o vmlinux -T arch/arm/kernel/vmlinux.lds。man ld,得到-T的意思是:为ld指定一个Linker script,意思是ld根据这个文件的内容来生成最终的二进制。

也许上面这个问题,你从没关注过,但是在研究内核代码的时候,常常有地方说“  __init宏会在最后的模块中生成一个特定的section,然后kernel加载的时候,寻找这个section中的函数”,说白了,上面这句话就是 说最后生成的模块中,有一个特定的section,这又是什么东西?

好吧,希望上面的问题勾起你的好奇心。下面我们来扫盲,最后会给一个链接地址,各看官可以去那深造。

一 section是什么?

好吧,我们需要解释一下平时编译链接生成的二进制可执行程序(比如说ELF,EXE也行),so或者dll,内核(非压缩的,参加本系列第一节内容、vmlinux),或者ko是怎么组织的。

其实,大家或多或少都知道这些二进制中包括有什么text/bss/data节(也叫section)。text节存储的是代码、data存储的是已经初始化的静态变量、bss节存储的是未初始化的什么东西...

上面的东西我就不细究了。反正一点,一个二进制,最终会包含很多section。那么,为什么section叫text/bss/data,能叫别的名字吗?

OK,可以。但是你得告诉ld,那么这些内容就通过-T选项指定一个linker script就行了。这些内容我们放到后面的实例中来介绍。

(再三强调,咱们在理论上只是抛砖引玉,希望有兴趣的看官自己研究,注意和我们分享你的成果就行了。)

二 link script基础知识介绍

linker script中的语法是linker command language(很简单的language,大家不用害怕...)。那么LS的目的是什么呢?

好了,下面我们介绍一些基本知识:

三 简单例子

下面来一个简单例子,


 SECTIONS

 {
       . = 0x10000;
       .text : { *(.text) }
       . = 0x8000000;
       .data : { *(.data) }
       .bss : { *(.bss) }
 }


你看,我们从这个LC文件中学到了什么?

恩,我们可以任意设置各个段的LMA值。当然,绝大部分情况,我们不需要有自己的LS来控制输出文件的内存布局。不过LK(linux kernel)可不一样了......

四 霸王硬上弓---vmlinux.lds.S分析

OK,有了上面的基础知识,下面我们霸王硬上弓,直接分析arch/arm/kernel/vmlinux.lds.S.虽然最终链接用的是vmlinux.lds,但是那个文件

由vmlinux.lds.S(这是一个汇编文件)得到,

arm-linux-gcc -E -Wp,-MD,arch/arm/kernel/.vmlinux.lds.d -nostdinc ...... -D__KERNEL__ -mlittle-endian ......

-DTEXT_OFFSET=0x00008000 -P -C -Uarm -D__ASSEMBLY__ -o arch/arm/kernel/vmlinux.lds      arch/arm/kernel/vmlinux.lds.S

所以,我们直接分析vmlinux.lds好了。

/*
   一堆注释,这里就不再贴上了,另外,增加//号做为注释标识
 * Convert a physical address to a Page Frame Number and back
 */

//OUTPUT_ARCH是LS语法中的COMMAND,用来指定输出文件的machine arch。objdump -f可查询所有支持的machine。另外

//这些东西涉及到一种叫BFD的。各位看官可以自己搜索下BFD的内容。

//下面这 表示输出文件基于ARM架构
OUTPUT_ARCH(arm)   

//ENTRY也是一个command,用来设置入口点。这里表示入口点是stext 。根据LD的描述,入口点的意思就是程序运行的第一条指令。内核是一个模块,大家把他想象

//成一个运行在硬件上的大程序就可以了。而我们的程序又是运行在内核至上的。比较下Java虚拟机以及运行在其上的Java程序吧......
ENTRY(stext)

//设置jiffies为jiffies_64
jiffies = jiffies_64;

//定义输出文件的段
SECTIONS
{

//设置location count为0xc0008000,这个好理解吧?内核运行的地址全在C0000000以上
 . = 0xC0000000 + 0x00008000;

//定义一个.text.head段,由输入文件中所有.text.head段组成

/*

LS语法中,关于seciton的定义如下:

section [address] [(type)] :
       [AT(lma)] [ALIGN(section_align)]
       [SUBALIGN(subsection_align)]
       [constraint]
       {
         output-section-command
         output-section-command
         ...
       } [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]

其中,address为VMA,而AT命令中的为LMA。一般情况,address不会设置,所以它默认等于当前的location counter

*/
 .text.head : {

/*这个非常关键,咱们在内核代码中经常能看到一些变量声明,例如extern int __stext,但是却找不到在哪定义的

其实这些都是在lds文件中定义的。这里得说一下编译链接相关的小知识。咱们这知道大概即可,具体内容可以自己深入研究

假设C代码中定义一个变量 int x = 0;那么

1 编译器首先会分配一块内存,用来存储该变量的值

2 编译器在程序的symbol表中,创建一项,用来存储这个变量的地址

例如,上面的 int x = 0,就在symbol表中创建一x项,这个x项指向一块内存,sizeof(int)大小,存储的值为0。当有地方使用这个x的时候,编译器会生成相应的代码,

首先指向这个x的内存,然后读取内存中的值。

上面的内容是C中一个变量的定义。但是Linker script中也可以定义变量,这时候只会生成一个symbol项,但是没有分配内存。。例如_stext=0x100,那么会

创建一个symbol项,指向0x100的内存,但该内存中没有存储value。所以,我们在C中使用LS中定义的变量的话,只能取它的地址。下面是一个例子:

start_of_ROM   = .ROM;
end_of_ROM     = .ROM + sizeof (.ROM) - 1;
start_of_FLASH = .FLASH;
上面三个变量是在LS中定义的,分别指向.ROM段的开始和结尾,以及FLASH段的开始。现在在C代码中想把ROM段的内容拷贝到FLASH段中,下面是C代码:
extern char start_of_ROM, end_of_ROM, start_of_FLASH;
memcpy (& start_of_FLASH, & start_of_ROM, & end_of_ROM - & start_of_ROM);

注意其中的取地址符号&。C代码中只能通过这种方式来使用LS中定义的变量. start_of_ROM这个值本身是没有意义的,只有它的地址才有意义。因为它的值没有初始化。

地址就指向.ROM段的开头。

说白了,LS中定义的变量其实就是地址,即_stext=0x100就是C代码中的一个地址 int *_stext=0x100。明白了?

最终的ld中会分配一个slot,然后存储x的地址。也就是说,ld知道这些勾当。那么当然我们在LS中

也可以定义一个变量,然后在C中使用了。所以下面这句话实际上定义了一个_stext变量。在C中通过extern就可以引用了。但是这里有一个

比较关键的问题。C中定义的x=0,其值被初始化为0了。也就是slot...待补充

*/
  _stext = .;.
  _sinittext = .;
  *(.text.head)
 }

//定义.init段,由所有的.init.text/.cpuinit.text/.meminit.text组成

//这时的LC的值为.init的开始
 .init : { /* Init code and data        */
   *(.init.text) *(.cpuinit.text) *(.meminit.text)

//定义一个变量 _einitext,它的值为当前的LC,即.init的初值+*(.init.text) *(.cpuinit.text) *(.meminit.text)的大小。也就是说变量

//_einitext标示一个结尾。
  _einittext = .;

//下面这个变量 __proc_info_begin标示一个开头 
  __proc_info_begin = .;
   *(.proc.info.init)  //所有.proc.info.init段内容在这
  __proc_info_end = .;//下面这个变量 __proc_info_end标示结尾,它和__proc_info_begin变量牢牢得把输出文件.proc.info.init的内容卡住了。

//有了上面begin和end的介绍,后面就简单了,大部分都是一个begin+end来卡住一段内容。根据前面的介绍,begin和end又可以在C程序中引用

//也就是我们通过Begin+end,就可以获得卡住的内容了。例如我们把一些初始化的函数指针放到一个begin和end中。然后通过一个循环,不就是

//可以调用这些函数了么。最后我们就来个例子介绍下。
  __arch_info_begin = .;
   *(.arch.info.init)
  __arch_info_end = .;
  __tagtable_begin = .;
   *(.taglist.init)
  __tagtable_end = .;
  . = ALIGN(16);
  __setup_start = .;
   *(.init.setup)
  __setup_end = .;
  __early_begin = .;
   *(.early_param.init)
  __early_end = .;
  __initcall_start = .;
   *(.initcallearly.init)

__early_initcall_end = .;
    *(.initcall0.init) *(.initcall0s.init) *(.initcall1.init)  *(.initcall1s.init) *(.initcall2.init) *(.initcall2s.init)  *(.initcall3.init) *(.initcall3s.init) *(.initcall4.init)  *(.initcall4s.init) *(.initcall5.init) *(.initcall5s.init)  *(.initcallrootfs.init)  *(.initcall6.init) *(.initcall6s.init) *(.initcall7.init)  *(.initcall7s.init)
  __initcall_end = .;
  __con_initcall_start = .;
   *(.con_initcall.init)
  __con_initcall_end = .;
  __security_initcall_start = .;
   *(.security_initcall.init)
  __security_initcall_end = .;
  . = ALIGN(32);//ALIGN,表示对齐,即这里的Location Counter的位置必须按32对齐
  __initramfs_start = .;   //ramfs的位置
   usr/built-in.o(.init.ramfs)
  __initramfs_end = .;
  . = ALIGN(4096); //4K对齐
  __per_cpu_load = .;
  __per_cpu_start = .;
   *(.data.percpu.page_aligned)
   *(.data.percpu)
   *(.data.percpu.shared_aligned)
  __per_cpu_end = .;
  __init_begin = _stext;
  *(.init.data) *(.cpuinit.data) *(.cpuinit.rodata) *(.meminit.data) *(.meminit.rodata)
  . = ALIGN(4096);
  __init_end = .;
 }

//DISACARD是一个特殊的section,表示符合这个条件的输入段都不会写到输出段中,也就是输出文件中不包含下列段
 /DISCARD/ : { /* Exit code and data        */
  *(.exit.text) *(.cpuexit.text) *(.memexit.text)
  *(.exit.data) *(.cpuexit.data) *(.cpuexit.rodata) *(.memexit.data) *(.memexit.rodata)
  *(.exitcall.exit)
  *(.ARM.exidx.exit.text)
  *(.ARM.extab.exit.text)
 }
//省略部分内容

//ADDR为内置函数,用来返回VMA的

/*

这里举个小例子,大家看看VMA和LMA到底有什么作用

SECTIONS
       {
       .text 0x1000 : { *(.text) _etext = . ; }  /.text段的VMA为0x1000,而且LMA=VMA
       .mdata 0x2000 : //.mdata段的VMA为0x2000,但是它的LMA却在.text段的结尾
         AT ( ADDR (.text) + SIZEOF (.text) )
         { _data = . ; *(.data); _edata = . ; }
       .bss 0x3000 :
         { _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
     }

看到了么?.mdata段运行的时候在0x2000,但是数据load地址却在.text段后,所以运行的时候需要把.mdata段内容拷贝过去。

 extern char _etext, _data, _edata, _bstart, _bend;
     char *src = &_etext;  //_etext为.text端的末尾 VMA地址,但同时也是.mdata段LMA的开始,有LS种的AT指定
     char *dst = &_data;  //_data为mdata段的VMA,现在需要把LMA地址开始的内容拷贝到VMA开始的地方

     /* ROM has data at end of text; copy it. */
     while (dst < &_edata)
       *dst++ = *src++;   //拷贝....明白了?不明白的好好琢磨

     /* Zero bss. */
     for (dst = &_bstart; dst< &_bend; dst++)
       *dst = 0;  //初始化数据区域

*/
 .rodata : AT(ADDR(.rodata) - 0) {
  __start_rodata = .; 
  *(.rodata) *(.rodata.*) *(__vermagic) *(__markers_strings) *(__tracepoints_strings) 
  } 
  .rodata1 : AT(ADDR(.rodata1) - 0) { 
  *(.rodata1) 
  } 
 ......//省略部分内容

  _edata_loc = __data_loc + SIZEOF(.data);
 .bss : {
  __bss_start = .; /* BSS                */
  *(.bss)
  *(COMMON)
  _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) }
}
//ASSERT是命令,如果第一个参数为0,则打印第二个参数的信息(也就是错误信息),然后ld命令退出。
ASSERT((__proc_info_end - __proc_info_begin), "missing CPU support")
ASSERT((__arch_info_end - __arch_info_begin), "no machine record defined")

五  内核代码中使用LS中定义的变量

咱们看一个小例子

[-->init/main.c]

extern initcall_t __initcall_start[], __initcall_end[], __early_initcall_end[]; //这几个值在LS中定义。大家可以在上面搜索下

static void __init do_initcalls(void)
{
    initcall_t *call;
    //上面已经定义成数组了,所以下面这些变量直接取的就是指针,和上面例子中使用&一个意思,反正不能用value
    for (call = __early_initcall_end; call < __initcall_end; call++)
        do_one_initcall(*call);

    /* Make sure there is no pending stuff from the initcall sequence */
    flush_scheduled_work();
}

关于Linux Kernel编译和链接中的linker script语法是怎样的就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

推荐阅读:
  1. 2.1Linux的 Directory Description目录描述(总结)
  2. linux程序移植到android上

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

linux kernel

上一篇:C++中怎么保留最大的数

下一篇:C#如何实现基于Socket套接字的网络通信封装

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》