MEMORY | INTERRUPT | TIMER | 并发与同步 | 进程管理 | 调度 | uboot | DTB | ARMV8 | ATF | Kernel Data Structure | PHY | LINUX2.6 | 驱动合集 | UART子系统 | USB专题 |

linux内核链接脚本vmlinux.lds分析(十一)

vmlinux.lds.S主要是用来组织内核的每个函数存放在内核镜像文件的位置。编译内核源码生成内核文件的过程分两步,一个是“编译”,另一个是“链接”的过程,vmlinux.lds.S要做的就是告诉编译器如何链接编译好的各个内核.o文件。未经编译的内核源码是不存在vmlinux.lds链接脚本的,在arch/arm/kernel目录只有vmlinux.lds.S文件,以及在include/asm-generi目录有一个与之关联的vmlinux.lds.h文件。在内核编译的时候会根据一些宏定义和传入的参数构建出针对特定平台、特定架构的vmlinux.lds链接脚本。

一. vmlinux.lds目标及其构建规则
在vmlinux的构建过程中,有提到过一个依赖 vmlinux-main,这里我们重点关注它的子层依赖 arch/arm/kernel/built-in.o。

|--- vmlinux-main
|      |--- core-y
|      |       |--- arch/arm/kernel/built-in.o

关于xxx/built-in.o的生成规则请移步到linux内核Makefile中的变量build— 过渡篇(五)。由参考文档可知:arch/arm/kernel/built-in.o的生成规则必定调用了以下的命令:

make -f scripts/Makefile.build obj=arch/arm/kernel

由于未指定目标,这时会使用Makefile.build中的默认目标__build。

# scripts/Makefile.build
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
	 $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
	 $(subdir-ym) $(always)
	@:

Makefile会首先寻找重建这些依赖的规则并构建它。而这些规则和依赖要么在当前的Makefile文件Makefile.build中,要么在Makefile.build包含的$ (obj)/Makefile中。这里重点关注 extra-y 依赖。首先找到依赖及其构建规则,如下:

# arch/arm/kernel/Makefile
extra-y := $(head-y) init_task.o vmlinux.lds

# scripts/Makefile.build
# Linker scripts preprocessor (.lds.S -> .lds)
# ---------------------------------------------------------------------------
quiet_cmd_cpp_lds_S = LDS     $@
      cmd_cpp_lds_S = $(CPP) $(cpp_flags) -P -C -U$(ARCH) \
	                     -D__ASSEMBLY__ -DLINKER_SCRIPT -o $@ $<

$(obj)/%.lds: $(src)/%.lds.S FORCE
	$(call if_changed_dep,cpp_lds_S)

可以明看到extra-y依赖于vmlinux.lds,vmlinux.lds依赖于vmlinux.lds.S,构建的规则是通过cmd_cpp_lds_S 命令。没什么好讲的,下面只给出编译时的具体打印:

arm-linux-gcc -E -Wp,-MD,arch/arm/kernel/.vmlinux.lds.d  -nostdinc -isystem /home/hh/opt/FriendlyARM/toolschain/4.5.1/bin/../lib/gcc/arm-none-linux-gnueabi/4.5.1/include
 -I/home/hh/linux-2.6.38/arch/arm/include -Iinclude  -include include/generated/autoconf.h -D__KERNEL__ 
 -mlittle-endian -Iarch/arm/mach-s3c64xx/include -Iarch/arm/plat-samsung/include
 -DTEXT_OFFSET=0x00008000 -P -C -Uarm -D__ASSEMBLY__ -DLINKER_SCRIPT 
 -o arch/arm/kernel/vmlinux.lds arch/arm/kernel/vmlinux.lds.S

小结:

																	  默认目标__build
																     /|          |--- KBUILD_BUILTIN
																    / |          |--- builtin-target
                                                                   /  |          |--- lib-target
vmlinux                                                           /   |          |--- extra-y
|--- vmlinux-main                                                /    |                 |--- head-y
|     |--- core-y                                               /     |                 |--- init_task.o
|     |     |--- arch/arm/kernel/built-in.o                    /      |                 |--- vmlinux.lds
                           \                                  /       |                            |--- vmlinux.lds.S  
                            \                                /
                             \						        /未指定目标,采用默认目标
                              \						       /			
             built-in.o的构建规则:make -f scripts/Makefile.build obj=arch/arm/kernel

二. vmlinux.lds的基础知识

1. 常用段的说明

段名称主要作用
init段linux定义的一种初始化过程中才会用到的段,一旦初始化完成,那么这些段所占用的内存会被释放掉,后续会继续说明
text段代码段,通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定
data段数据段,通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配
bss段通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态内存分配

2. 运行地址、链接地址、加载地址、存储地址、位置无关码/位置相关码的说明

2.1 运行地址
程序实际在内存中运行时候的地址,比如CPU要执行一条指令,那么必然要通过给PC赋值,从对应的地址空间中去取出来,那么这个地址就是实际的运行地址。更简单的讲,就是要寻址到一个指令或者变量所使用的地址。运行地址是动态的,如果你将程序加载到内存中时,改变存放在内存的地址,那么运行地址也就随之改变了。

2.2 加载地址/存储地址
对于加载地址和存储地址的概念,我认为它们是等价的概念(个人观点),指的是当前运行的程序存放的地址。有一篇博文 运行地址,链接地址,加载地址,存储地址 位置无关码、位置有关码提到:
“加载地址<—>存储地址:他们两个是等价的,也是两种不同的说法” 然后又提到 “加载地址:程序保存在Nand flash中的地址” 我认为这两句的解释是有待商榷的!特别是后面一句的解释,为什么?假设当前开发板是从nor flash启动(此时片内可执行),可不可以理解加载和存储都在nor flash中?假设经过代码重定位后代码被搬移到内存中指定的位置,此时可不可以理解接下来程序的运行加载和存储都在内存中?”

2.3 链接地址
程序在编译器编译成可执行文件时(四个过程:预处理、编译、汇编、链接),在链接过程中我们指定的地址。理论上程序运行时所处的地址。链接地址是静态的,在进行程序编译的时候指定的。

2.4 位置无关码/位置相关码
位置无关可执行文件PIE (Position-Independent-Executable)包括位置无关代码PIC和位置无关数据PID两部分:
其中:

  • 位置无关码: PIC(Position independent code),在程序被编译成二进制可执行文件后,其指令的运行与内存地址无关。依赖于程序当前运行的PC值,进行相对的跳转。
  • 位置有关码: 和位置无关码相反,也就是编译后指令的运行和内存地址有关。不依赖当前PC值,是绝对跳转,只有程序运行在链接地址处时,才能正确执行。

总结: 位置无关代码:mov、b或bl等指令,在跳转时,地址是PC+相对偏移量,这样的指令没有绝对地址,都是相对PC的偏移,这样,即使运行地址和链接地址不一样,也不影响实际代码的执行。位置相关代码:ldr r0, =lable,这里的标签实际指的就是链接后地址,这条指令实质就是将标号的地址放到PC里实现跳转,但是如果实际运行的地址和链接的地址不一样,这样就会由于跳转的地址不是实际运行地址而出错,这就叫做位置相关代码。如果你的这段代码需要实现位置无关,那么你就不能使用绝对寻址指令,否则的话就是位置有关了。

所以:

  • 当 链接地址 == 运行地址时,整个程序运行正常;
  • 当 链接地址 != 运行地址时,如果整个代码中的地址都是相对偏移量(使用位置无关指令),那么整个程序仍然运行畅通无阻,否则,整个程序运行结果就会出错(因为指定运行地址和实际运行地址不符)。

--------------------------------------------- 举例 -----------------------------------------------
我使用的是mini6410开发板,链接地址指定为0x5fe00000,内存起始地址为0x50000000。上电开始后,nandflash的前8k代码被自动映射到内部ram的0~0x2000处。假设有如下两条汇编指令:

	bl disable_watchdog	
	ldr pc, =disable_watchdog

反汇编后:

...
5fe0008c:	eb000011 	bl	5fe000d8 <disable_watchdog>
5fe00090:	e59ff2f8 	ldr	pc, [pc, #760]	; 5fe00390 <_mmu_table_base+0x4>
...
5fe000d8 <disable_watchdog>:
5fe000d8:	e59f02b8 	ldr	r0, [pc, #696]	; 5fe00398 <_mmu_table_base+0xc>
5fe000dc:	e3a01000 	mov	r1, #0
5fe000e0:	e5801000 	str	r1, [r0]
5fe000e4:	e1a0f00e 	mov	pc, lr
...
5fe0038c <_mmu_table_base>:
5fe0038c:	5fe04000 	svcpl	0x00e04000
5fe00390:	5fe000d8 	svcpl	0x00e000d8
...

(1)bl disable_watchdog

# 反汇编后
5fe0008c:	eb000011 	bl	5fe000d8 <disable_watchdog>

bl 是相对跳转:PC + 偏移值 (PC值等于当前地址+8)

偏移值:机器码 0xeb000011 低24位 0x000011按符号为扩展为32 位 0x00000011的 正数,向后跳转 0x11 个 4字节 也就是 0x44

假设当前运行地址0x0000008c:
<1> pc = 0x0000008c
<2> pc_new = 0x0000008c + 8 + 0x44 = 0x000000d8 正常运行

假设当前运行地址0x5fe0008c(只针对重定位后的情景,为什么?不重定位内存里哪来的代码):
<1>pc = 0x5fe0008c
<2>pc_new = 0x5fe0008c+ 8 + 0x44 = 5fe000d8 正常运行

(2)ldr pc, =disable_watchdog

# 反汇编后
5fe00090:	e59ff2f8 	ldr	pc, [pc, #760]	; 5fe00390 <_mmu_table_base+0x4>

假设当前运行地址0x0000008c:
<1> pc = 0x0000008c
<2> pc_new = *(0x0000008c + 8 + 760) = *(0x0000038c) = 0x5fe000d8
代码是否重定位?如果没有,鬼知道0x5fe000d8地址处存放了什么内容,程序跑飞没商量。如果有,那就继续正常运行呗

假设当前运行地址0x5fe0008c(同样只针对重定位后的情景):
<1> pc = 0x5fe0008c
<2> pc_new = *(0x0000008c + 8 + 760) = *(0x5fe0038c) = 0x5fe000d8 正常运行

--------------------------------------------- 举例结束 -----------------------------------------------

补充知识:为什么pc值是当前地址+8?

先提一下流水线技术这个概念:通过多个功能部件并行工作来缩短程序执行时间,提高处理器核的效率和吞吐率。ARM处理器使用流水线来增加处理器指令流的速度,这样可使几个操作同时进行,并使处理与存储器系统之间的操作更加流畅,连续。

以流水线使用三个阶段为例(ARM7):
PC代表程序计数器,流水线使用三个阶段,因此指令分为三个阶段执行:

  • 1.取指(从存储器装载一条指令);
  • 2.译码(识别将要被执行的指令);
  • 3.执行(处理指令并将结果写回寄存器)。
    PC总是指向“正在取指”的指令,而不是指向“正在执行”的指令或正在“译码”的指令。一般来说,人们习惯性约定将“正在执行的指令作为参考点”,称之为当前第一条指令,因此PC总是指向第三条指令。当ARM状态时,每条指令为4字节长(Thumb指令每条指令为2字节),所以PC始终指向该指令地址加8字节的地址,即:PC值=当前程序执行位置+8;

ARM流水线解析
ARM9——五级流水线结构,以及PC指针
从ARM流水线理解ARM的PC指针

怎么实现位置无关码?

  • 位置无关的函数跳转
  • 位置无关的常量访问

为什么在这里花费了大量的篇幅讲这个位置无关码/位置相关码?是因为我在写bootloder程序时,未重定位代码之前,实现串口打印字符串常量的功能,发现这个字符串总是打印不出来。后来想想才明白因为当前运行的地址在内部ram的前8k,链接地址是0x5fe00000,这个时候去访问了一个位置有关的常量。。。。。

3. 链接器中的entry
链接脚本中有几种设置入口点的方法,请参考链接脚本说明手册。链接器将按顺序尝试以下每一种方法来设置入口点,当其中一种方法成功时停止:

  • 1 -e 命令行选项
  • 2 脚本中的entry(symbol)命令
  • 3 如定义了start的值,取其值为入口点
  • 4 text段的第一个字节的地址
  • 5 地址0

三. vmlinux.lds.S的分析

1. 指定输出文件架构格式和入口地址

OUTPUT_ARCH(arm) //输出格式基于arm架构
ENTRY(stext)     //指定stext作为,程序的入口点

2. 指定内存虚拟地址空间的起始地址和内核映像存放的偏移地址

. = PAGE_OFFSET + TEXT_OFFSET

(1) 内存虚拟地址空间的起始地址 - - -PAGE_OFFSET
在arch/arm/include/asm/memory.h中的文件定义了PAGE_OFFSET :

#define PAGE_OFFSET		UL(CONFIG_PAGE_OFFSET)

其中CONFIG_PAGE_OFFSET是在内核配置里面配置的,不同开发板会有所不同。我的配置(mini6410):

#
# Kernel Features
#
CONFIG_VMSPLIT_3G=y
# CONFIG_VMSPLIT_2G is not set
# CONFIG_VMSPLIT_1G is not set
CONFIG_PAGE_OFFSET=0xC0000000

(2) 内核映像存放的偏移地址 - - -TEXT_OFFSET
在arch/arm/Makefile中有定义:

textofs-y	:= 0x00008000
...
# The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)

所以:. = PAGE_OFFSET + TEXT_OFFSET就变成为:.=c0008000,这个地址就是内核映像存放在内存的虚拟的起始地址。

内核映像存放地址为什么要偏移0x00008000?前面的32k用来做什么?
因为kernel镜像的前面16K需要预留出来给初始化页表项使用。对应代码arch/arm/kernel/head.S,这里的注释也有提到。我将在内核启动系列篇对这16k如何使用给出详细的解释。这里只简单说明一下。

/*
 * swapper_pg_dir is the virtual address of the initial page table.
 * We place the page tables 16K below KERNEL_RAM_VADDR.  Therefore, we must
 * make sure that KERNEL_RAM_VADDR is correctly set.  Currently, we expect
 * the least significant 16 bits to be 0x8000, but we could probably
 * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
 */
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif

3. init 段
3.1 _sinittext ~ _einittext 之间的段

_sinittext = .;
	HEAD_TEXT
	INIT_TEXT
	ARM_EXIT_KEEP(EXIT_TEXT)
_einittext = .;

其中HEAD_TEXT, INIT_TEXT,ARM_EXIT_KEEP(EXIT_TEXT)这些宏定义在:include/asm-generic/vmlinux.lds.h中。

/* Section used for early init (in .S files) */
#define HEAD_TEXT  *(.head.text)

#define INIT_TEXT							\
	*(.init.text)							\
	DEV_DISCARD(init.text)						\
	CPU_DISCARD(init.text)						\
	MEM_DISCARD(init.text)

#if defined(CONFIG_SMP_ON_UP) && !defined(CONFIG_DEBUG_SPINLOCK)
#define ARM_EXIT_KEEP(x)	x
#else
#define ARM_EXIT_KEEP(x)
#endif

3.2 (.proc.info.init),(.arch.info.init), (.taglist.init),(.alt.smp.init)等段

		ARM_CPU_DISCARD(PROC_INFO)
		__arch_info_begin = .;
			*(.arch.info.init)
		__arch_info_end = .;
		__tagtable_begin = .;
			*(.taglist.init)
		__tagtable_end = .;
#ifdef CONFIG_SMP_ON_UP          //一般多核arm芯片都是smp的
		__smpalt_begin = .;
			*(.alt.smp.init)
		__smpalt_end = .;
#endif

		INIT_SETUP(16)

		INIT_CALLS
		CON_INITCALL
		SECURITY_INITCALL
		INIT_RAM_FS

紧接着以此存放的是:(.proc.info.init),(.arch.info.init)(.taglist.init),(.alt.smp.init),以及一些initcall的段代码,关于这些段的定义非常重要,我将放在接下来的两篇给出详细的介绍。

3.1~3.2属于初始化专用的数据段。从__init_begin开始,至__init_end结束。.init段中的代码段和数据段,在Linux初始化完成之后,这个段的内存都会被请空,被释放。因为他们只需要在初始化的时候使用一次,没有必要再驻留在内存中,浪费空间。

#ifndef CONFIG_XIP_KERNEL
		__init_begin = _stext;
		INIT_DATA
		ARM_EXIT_KEEP(EXIT_DATA)
#endif
	}

	PERCPU(PAGE_SIZE)

#ifndef CONFIG_XIP_KERNEL
	. = ALIGN(PAGE_SIZE);
	__init_end = .;
#endif

3.3 启动后会驻留在内存中的内核代码段

	.text : {			/* Real text segment		*/
		_text = .;		/* Text and read-only data	*/
			__exception_text_start = .;
			*(.exception.text)
			__exception_text_end = .;
			IRQENTRY_TEXT
			TEXT_TEXT
			SCHED_TEXT
			LOCK_TEXT
			KPROBES_TEXT
#ifdef CONFIG_MMU
			*(.fixup)
#endif
			*(.gnu.warning)
			*(.rodata)
			*(.rodata.*)
			*(.glue_7)
			*(.glue_7t)
		. = ALIGN(4);
		*(.got)			/* Global offset table		*/
			ARM_CPU_KEEP(PROC_INFO)
	}

	RO_DATA(PAGE_SIZE)

#ifdef CONFIG_ARM_UNWIND
	/*
	 * Stack unwinding tables
	 */
	. = ALIGN(8);
	.ARM.unwind_idx : {
		__start_unwind_idx = .;
		*(.ARM.exidx*)
		__stop_unwind_idx = .;
	}
	.ARM.unwind_tab : {
		__start_unwind_tab = .;
		*(.ARM.extab*)
		__stop_unwind_tab = .;
	}
#endif

	_etext = .;			/* End of text and rodata section */

所有_text 开始,_etext结束区间的段都是内核代码段。

3.4 已经初始化的数据段

#ifdef CONFIG_XIP_KERNEL    //一般都是未定义
	__data_loc = ALIGN(4);		/* location in binary */
	. = PAGE_OFFSET + TEXT_OFFSET;
#else
	. = ALIGN(THREAD_SIZE);
	__data_loc = .;
#endif

	.data : AT(__data_loc) {
		_data = .;		/* address in memory */
		_sdata = .;

		/*
		 * first, the init task union, aligned
		 * to an 8192 byte boundary.
		 */
		INIT_TASK_DATA(THREAD_SIZE)

#ifdef CONFIG_XIP_KERNEL  //一般都是未定义
		. = ALIGN(PAGE_SIZE);
		__init_begin = .;
		INIT_DATA
		ARM_EXIT_KEEP(EXIT_DATA)
		. = ALIGN(PAGE_SIZE);
		__init_end = .;
#endif

		NOSAVE_DATA
		CACHELINE_ALIGNED_DATA(32)
		READ_MOSTLY_DATA(32)

		/*
		 * The exception fixup table (might need resorting at runtime)
		 */
		. = ALIGN(32);
		__start___ex_table = .;
#ifdef CONFIG_MMU   //一般都要定义
		*(__ex_table)
#endif
		__stop___ex_table = .;

		/*
		 * and the usual data section
		 */
		DATA_DATA
		CONSTRUCTORS

		_edata = .;
	}
	_edata_loc = __data_loc + SIZEOF(.data);

3.5 bss段

	NOTES

	BSS_SECTION(0, 0, 0)
	_end = .;

bss段是未经初始化的内核数据段.关于它的使用我也将放在内核启动系列篇给出详细的解释。

posted on 2022-11-02 22:23  BSP-路人甲  阅读(1203)  评论(0编辑  收藏  举报

导航