关于linux可安装模块的装载地址的研究

前言、写这篇文章的由来

      最近在学习韦东山嵌入式培训视频(3期项目实战之USB摄像头监控)时,仿照视频教程,自己写了一个简化版的uvc摄像头驱动。在虚拟机上调试驱动模块时,入了一个大坑,折腾了很久才总算爬出来。而源头是由于自己对linux模块的装载地址认识不足,再加上一篇网文的误导。兹整理记录下来爬坑期间做的笔记,希望对自己和大家都有所助益。

一、实验环境

1.1 虚拟机环境

a) Vmware版本:Vmware Workstation 12.5.7

b) Ubuntu版本:9.10

c) 内核版本:2.6.31.14

d) gcc版本:4.4.1

e) gdb版本:7.0

1.2 开发板环境

1.2.1 硬件

开发板:百问网JZ2440开发板

摄像头:百问网自制uvc摄像头

1.2.2 软件

a) 内核版本: 2.6.31.14

b) toolchain版本:

  arm-linux-gcc 3.4.5

  arm-linux-gdb 6.8

二、 基础知识简介

2.1 linux可安装模块(简称LKM)

      LKM是一种特殊的ELF文件。可以用objdmp –h 或者readelf -e来查看它的section信息。

      LKM不能直接运行,而需要用insmod命令动态装载到内核中,才能运行。而内核装载LDM主要是在load_module()中实现的。它会为LKM分配内存空间,计算其各个section的装载地址,然后将它们复制到对应的装载地址上。

2.2 关于怎样用kgdb调试可安装模块,网上有很多文章。总结下来,基本套路都是类似的,大致如下:   

      i)配置目标机的内核,以支持kgdb调试

      ii)配置目标机的引导程序的启动参数,以支持kgdb串口调试

      iii) 目标机上insmod xxx.ko,然后用某种方法获取xxx.ko的text节的装载地址(假设为0xd099a000)(注:请见下文3.3)

      iv) 目标机在shell中运行echo g > /proc/sysrq-trigger ,等待开发机的gdb来连接

      v) 开发机在shell中进入linux源代码目录,然后执行:

        gdb ./vmlinux

        (gdb) set remotebaud 115200 

        (gdb) target remote /dev/ttyS0

        Remote debugging using /dev/ttyS0

        kgdb_breakpoint () at kernel/kgdb.c:1674

        1674 wmb(); /*Sync point after breakpoint */

        以上信息说明开发机的gdb和目标机的kgdb成功建立了连接。接下来,在开发机上运行:

        (gdb) add-symbol-file /xxx_path/xxx.ko 0xd099a000

      后续的调试方法,就和在单机上用gdb调试应用程序基本相同了。

2.3 关于怎样获取模块的代码段的装载地址,网上有两种方法:

      方法1)经验证是可行的

      cat /sys/module/xxx.ko/sections/.text

      方法2)经验证是有问题的

      “cat /proc/modules | grep test”(假设模块名称为test.ko,注意不要带".ko")找到模块的加载地址,如下图所示:

      wps_clip_image-25837_thumb1

      找到代码段(.text段)的偏移量,如下图所示:

      wps_clip_image-20364_thumb1

      偏移量是.text对应的行中第四个十六进制字段(或者说从左往右数第六个字 段)是.text段在文件中的偏移量。将这一偏移量与模块中的装载地址相加,就可以找到模块的代码在重定向之后的地址了。在我们的例子中,可以得到 0xffffffffa001b000 + 0x00000040 = 0xFFFFFFFFA001B040。 

三、为什么第二种方法是有问题的

3.1 内核源代码的分析

      (下文分析的内核版本是2.6.31.14)

       模块的加载,是在kernel/module.c的load_module()里实现的:

static noinline struct module *load_module(void __user *umod,
				  unsigned long len,
				  const char __user *uargs)
{
    . . .

    /* Determine total sizes, and put offsets in sh_entsize.  For now
    this is done generically; there doesn't appear to be any
    special cases for the architectures. */
    layout_sections(mod, hdr, sechdrs, secstrings);

    . . .

    /* Transfer each section which specifies SHF_ALLOC */
    DEBUGP("final section addresses:\n");
    for (i = 0; i < hdr->e_shnum; i++) {
        void *dest;
        if (!(sechdrs[i].sh_flags & SHF_ALLOC))
	    continue;
        if (sechdrs[i].sh_entsize & INIT_OFFSET_MASK)
	    dest = mod->module_init	+ (sechdrs[i].sh_entsize & ~INIT_OFFSET_MASK);
        else
	    dest = mod->module_core + sechdrs[i].sh_entsize;

        if (sechdrs[i].sh_type != SHT_NOBITS)
             memcpy(dest, (void *)sechdrs[i].sh_addr,
                    sechdrs[i].sh_size);
         /* Update sh_addr to point to copy in image. */
         sechdrs[i].sh_addr = (unsigned long)dest;
        DEBUGP("\t0x%lx %s\n", sechdrs[i].sh_addr, secstrings + sechdrs[i].sh_name);

    }
}

      模块里各个section的定位,主要就是在以上两段代码中实现的。

- 第一段代码layout_sections()的作用,正如注释所说:a)确定模块总大小,b) 计算模块里各个section的偏移量,并保存到sechdrs.sh_entsize里(内核会为模块分
   配一个Elf_Shdr类型的数组sechdrs[],数组元素分别对应该模块的每个section的头信息)

- 第二段代码的作用,是对模块内带有SHF_ALLOC标志的section,将其内容复制到最终的目的地址上。

      下面主要分析layout_sections():

/* Lay out the SHF_ALLOC sections in a way not dissimilar to how ld
   might -- code, read-only data, read-write data, small data.  Tally
   sizes, and place the offsets into sh_entsize fields: high bit means it
   belongs in init. */
static void layout_sections(struct module *mod,
			    const Elf_Ehdr *hdr,
			    Elf_Shdr *sechdrs,
			    const char *secstrings)
{
	static unsigned long const masks[][2] = {  //关于这些mask的含义,暂未深究,详见参考资料5.8(ELF格式探析之三:sections):
		/* NOTE: all executable code must be the first section
		 * in this array; otherwise modify the text_size
		 * finder in the two loops below */
		{ SHF_EXECINSTR | SHF_ALLOC, ARCH_SHF_SMALL },
		{ SHF_ALLOC, SHF_WRITE | ARCH_SHF_SMALL },
		{ SHF_WRITE | SHF_ALLOC, ARCH_SHF_SMALL },
		{ ARCH_SHF_SMALL | SHF_ALLOC, 0 }
	};
	unsigned int m, i;

	for (i = 0; i < hdr->e_shnum; i++)
		sechdrs[i].sh_entsize = ~0UL; //先初始化sh_entsize 为-1

	DEBUGP("Core section allocation order:\n");
	for (m = 0; m < ARRAY_SIZE(masks); ++m) {
		for (i = 0; i < hdr->e_shnum; ++i) {
			Elf_Shdr *s = &sechdrs[i];

			if ((s->sh_flags & masks[m][0]) != masks[m][0]
			    || (s->sh_flags & masks[m][1])
			    || s->sh_entsize != ~0UL
			    || strstarts(secstrings + s->sh_name, ".init")) //对每个section进行一些判断,细节暂未深究
				continue;
			s->sh_entsize = get_offset(mod, &mod->core_size, s, i); //计算各个section的偏移量,并保存到sechdrs.sh_entsize里
			DEBUGP("\t%s\n", secstrings + s->sh_name);
		}
		if (m == 0)
			mod->core_text_size = mod->core_size;
	}

	DEBUGP("Init section allocation order:\n");
        . . . //省略和init节相关的处理代码
    }
}

      可见,核心的代码就是get_offset()这个函数:

/* Update size with this section: return offset. */
static long get_offset(struct module *mod, unsigned int *size,
		       Elf_Shdr *sechdr, unsigned int section)
{
	long ret;

	*size += arch_mod_section_prepend(mod, section); //在非parisc平台下,arch_mod_section_prepend定义为return 0;
	ret = ALIGN(*size, sechdr->sh_addralign ?: 1);//对齐修正。注意:这里的三目表达式等价于sechdr->sh_addralign ? sechdr->sh_addralign : 1
	*size = ret + sechdr->sh_size;
	return ret;
}

      从代码可以看出:内核在计算LKM装载后的各个section的偏移地址时,并没有用.ko文件中的offset信息,而是按照以下公式来计算的:

offset_align = ALIGN(*size, sechdr->sh_addralign ?: 1)

其中,offset_align是计算出的偏移地址,*size是该section之前所有section的总大小,ALIGN是进行对齐修正,对齐边界是该section的sh_addralign或者1。

     对于.text节来说,由于它是模块的第一个section,所以*size即mod->core_size,且因mod->core_size初始值是0(这是我在单步时确定的,但我没找到源代码中对其显式初始化的地方?),所以text节的offset_align即等于0,换句话说,text节的装载地址就是模块的装载地址。


3.2 实验验证(在JZ2440开发板上实验)

step1) 在get_offset()里加printk后,编译安装内核

step2)insmod my_uvc.ko,然后cat /proc/modules|grep my_uvc获取模块的装载地址,然后通过sysfs获取各个section的装载地址

#cat /proc/modules |grep my_uvc
my_uvc 15028 0 - Live 0xbf000000

# ls -al /sys/module/my_uvc/sections/
drwxr-xr-x    2 0        0               0 Jan  1 00:04 .
drwxr-xr-x    6 0        0               0 Jan  1 00:01 ..
-r--r--r--    1 0        0            4096 Jan  1 00:04 .bss
-r--r--r--    1 0        0            4096 Jan  1 00:04 .data
-r--r--r--    1 0        0            4096 Jan  1 00:04 .gnu.linkonce.this_module
-r--r--r--    1 0        0            4096 Jan  1 00:04 .note.gnu.build-id
-r--r--r--    1 0        0            4096 Jan  1 00:04 .rodata
-r--r--r--    1 0        0            4096 Jan  1 00:04 .rodata.str1.1
-r--r--r--    1 0        0            4096 Jan  1 00:04 .strtab
-r--r--r--    1 0        0            4096 Jan  1 00:04 .symtab
-r--r--r--    1 0        0            4096 Jan  1 00:04 .text

# cat /sys/module/my_uvc/sections/.text
0xbf000000
# cat /sys/module/my_uvc/sections/.note.gnu.build-id
0xbf0013e8
# cat /sys/module/my_uvc/sections/.rodata
0xbf00140c
# cat /sys/module/my_uvc/sections/.rodata.str1.1
0xbf0015c4
# cat /sys/module/my_uvc/sections/.symtab
0xbf001a08
# cat /sys/module/my_uvc/sections/.strtab
0xbf002438
# cat /sys/module/my_uvc/sections/.gnu.linkonce.this_module
0xbf002a70
# cat /sys/module/my_uvc/sections/.bss
0xbf002b84

对比printk的日志(注:红字是我手工加上的结算结果)

sh_name:50, name:.text, sh_type:1, sh_flags:6, sh_offset:88, sh_size:5096, sh_addralign:4, offset_align:0
0xbf000000+0=0xbf000000

sh_name:27, name:.note.gnu.build-id, sh_type:7, sh_flags:2, sh_offset:52, sh_size:36, sh_addralign:4, offset_align:0x13e8
0xbf000000+0x13e8=0xbf0013e8

sh_name:60, name:.rodata, sh_type:1, sh_flags:2, sh_offset:5184, sh_size:440, sh_addralign:4, offset_align:0x140c
0xbf000000+0x140c=0xbf00140c

sh_name:77, name:.rodata.str1.1, sh_type:1, sh_flags:50, sh_offset:5682, sh_size:1092, sh_addralign:1, offset_align:0x15c4
0xbf000000+0x15c4=0xbf0015c4

sh_name:1, name:.symtab, sh_type:2, sh_flags:2, sh_offset:130584, sh_size:2608, sh_addralign:4, offset_align:0x1a08
0xbf000000+0x1a08=0xbf001a08

sh_name:9, name:.strtab, sh_type:3, sh_flags:2, sh_offset:133192, sh_size:1397, sh_addralign:1, offset_align:0x2438
0xbf000000+0x2438=0xbf002438

i:8, sh_name:96, name:.data, sh_type:1, sh_flags:3, sh_offset:6776, sh_size:192, sh_addralign:4, offset_align:0x29b0
0xbf000000+0x29b0=0xbf0029b0

i:10, sh_name:106, name:.gnu.linkonce.this_module, sh_type:1, sh_flags:3, sh_offset:6968, sh_size:276, sh_addralign:4, offset_align:0x2a70
0xbf000000+0x2a70=0xbf002a70

i:12, sh_name:132, name:.bss, sh_type:8, sh_flags:3, sh_offset:7244, sh_size:3888, sh_addralign:4, offset_align:0x2b84
0xbf000000+0x2b84=0xbf002b84

可见,实际结果和分析源代码得出的结论是一致的。

四、如果使用第二种方法获取LKM的text节的装载地址,实际调试时,会表现出什么症状,原因是什么

4.1 实践发现,在X86平台下,和在arm平台下,实际调试时表现出的症状,差别比较大

1) 在X86平台下面,会出现很多奇怪的现象,例如:

    - 虽然加了断点,但continue让程序跑起来后,却直接无视断点;

    - 有时虽然能进断点,但单步之后,程序就跑飞掉了;

    - 甚至加了断点,continue让程序跑起来后,会报segmentation fault,或者instruction fault

2) 在arm平台下,可以正常的调试(包括加断点,单步,监视等),没有表现出显式的症状

4.2 为什么在两种平台下,会有这种差别呢?

    为了找出原因,尝试在两个平台下分别做以下实验(为了简化,目标代码用了宋宝华的《Linux设备驱动开发详解:基于最新的Linux 4.0内核》第六章的globalmem):

step1)用两种方法(详见上文2.3小节),分别获取LKM的text节的装载地址;

step2)在gdb中,分别用这两种地址来add-symbol-file,然后b globalmem_read(第二种不要continue),第一种当continue命中断点后,打印反汇编代码,结果是:

a) X86平台(假设cat /proc/modules|grep globalmem得到LKM的装载地址是0xe0c26000,而objdump –h 得到text节的offset是40)  

    用第二种方法获取text节的装载地址,然后add-symbol-file,b globalmem_read:

(gdb) add-symbol-file /home/book/workspace/baohua/drivers/ch6/globalmem.ko  0xe0c26040
add symbol table from file "/home/book/workspace/baohua/drivers/ch6/globalmem.ko" at
	.text_addr = 0xe0c26040
(y or n) y
Reading symbols from /home/book/workspace/baohua/drivers/ch6/globalmem.ko...done.
(gdb) b globalmem_read
Breakpoint 1 at 0xe0c261a0: file /home/book/workspace/baohua/drivers/ch6/globalmem.c, line 64.

    用第一种方法获取text节的装载地址,然后add-symbol-file,b globalmem_read:

(gdb) add-symbol-file /home/book/workspace/baohua/drivers/ch6/globalmem.ko  0xe0c26000
add symbol table from file "/home/book/workspace/baohua/drivers/ch6/globalmem.ko" at
	.text_addr = 0xe0c26000
(y or n) y
Reading symbols from /home/book/workspace/baohua/drivers/ch6/globalmem.ko...done.

(gdb) b globalmem_read
Breakpoint 1 at 0xe0c26174: file /home/book/workspace/baohua/drivers/ch6/globalmem.c, line 70.

(gdb) c
Continuing.
[New Thread 3101]
[Switching to Thread 3101]

Breakpoint 1, globalmem_read (filp=0xda625b00,
    buf=0x9163000 <Address 0x9163000 out of bounds>, size=32768, ppos=0xd7ae9f98)
    at /home/book/workspace/baohua/drivers/ch6/globalmem.c:70
70		if (p >= GLOBALMEM_SIZE)
(gdb)disassemble
0xe0c26160 <globalmem_read+0>:	push   %ebp
0xe0c26161 <globalmem_read+1>:	mov    %esp,%ebp
0xe0c26163 <globalmem_read+3>:	sub    $0x18,%esp
0xe0c26166 <globalmem_read+6>:	mov    %ebx,-0xc(%ebp)
0xe0c26169 <globalmem_read+9>:	mov    %esi,-0x8(%ebp)
0xe0c2616c <globalmem_read+12>:	mov    %edi,-0x4(%ebp)
0xe0c2616f <globalmem_read+15>:	nopl   0x0(%eax,%eax,1)
0xe0c26174 <globalmem_read+20>:	xor    %esi,%esi        ;可见,用第一种方法获取的地址做实验,断点的位置是正确的
0xe0c26176 <globalmem_read+22>:	mov    0x70(%eax),%eax
0xe0c26179 <globalmem_read+25>:	mov    %edx,%edi
0xe0c2617b <globalmem_read+27>:	mov    0x8(%ebp),%edx
0xe0c2617e <globalmem_read+30>:	mov    (%edx),%ebx
0xe0c26180 <globalmem_read+32>:	cmp    $0xfff,%ebx
0xe0c26186 <globalmem_read+38>:	ja     0xe0c261a8 <globalmem_read+72>
0xe0c26188 <globalmem_read+40>:	mov    $0x1000,%si
0xe0c2618c <globalmem_read+44>:	sub    %ebx,%esi
0xe0c2618e <globalmem_read+46>:	cmp    %ecx,%esi
0xe0c26190 <globalmem_read+48>:	ja     0xe0c261b8 <globalmem_read+88>
0xe0c26192 <globalmem_read+50>:	lea    0x3c(%eax,%ebx,1),%edx
0xe0c26196 <globalmem_read+54>:	mov    %esi,%ecx
0xe0c26198 <globalmem_read+56>:	mov    %edi,%eax
0xe0c2619a <globalmem_read+58>:	call   0xc0326d60 <copy_to_user>
0xe0c2619f <globalmem_read+63>:	test   %eax,%eax
    ;可见,用第二种方法获取的地址做实验,由于在代码段中,0xe0c261a0这个地址上并没有有效的指令,所以如果在其上加断点,就会产生各种奇怪的问题
0xe0c261a1 <globalmem_read+65>:	je     0xe0c261bc <globalmem_read+92>
0xe0c261a3 <globalmem_read+67>:	mov    $0xfffffff2,%esi
0xe0c261a8 <globalmem_read+72>:	mov    %esi,%eax
0xe0c261aa <globalmem_read+74>:	mov    -0xc(%ebp),%ebx
0xe0c261ad <globalmem_read+77>:	mov    -0x8(%ebp),%esi
0xe0c261b0 <globalmem_read+80>:	mov    -0x4(%ebp),%edi
0xe0c261b3 <globalmem_read+83>:	mov    %ebp,%esp
0xe0c261b5 <globalmem_read+85>:	pop    %ebp
0xe0c261b6 <globalmem_read+86>:	ret

b) arm平台(假设cat /proc/modules|grep globalmem得到LKM的装载地址是0xbf000000,而objdump –h 得到text节的offset是34)  

    用第二种方法获取text节的装载地址,然后add-symbol-file,b globalmem_read:

(gdb) add-symbol-file /home/book/workspace/baohua/drivers/ch6/globalmem.ko  0xbf000034
add symbol table from file "/home/book/workspace/baohua/drivers/ch6/globalmem.ko" at
	.text_addr = 0xbf000034
(y or n) y
Reading symbols from /home/book/workspace/baohua/drivers/ch6/globalmem.ko...done.
(gdb) b globalmem_read
Breakpoint 1 at 0xbf000200: file /home/book/workspace/baohua/drivers/ch6/globalmem.c, line 65.

    用第一种方法获取text节的装载地址,然后add-symbol-file,b globalmem_read:

(gdb) add-symbol-file /home/book/workspace/baohua/drivers/ch6/globalmem.ko  0xbf000000
add symbol table from file "/home/book/workspace/baohua/drivers/ch6/globalmem.ko" at
	.text_addr = 0xbf000000
(y or n) y
Reading symbols from /home/book/workspace/baohua/drivers/ch6/globalmem.ko...done.

(gdb) b globalmem_read
Breakpoint 1 at 0xbf0001cc: file /home/book/workspace/baohua/drivers/ch6/globalmem.c, line 65.

(gdb) c
Continuing.
[New Thread 787]
[Switching to Thread 787]

Breakpoint 1, globalmem_read (filp=0xc3df44e0, buf=0xbe962c80 "", size=8192,
    ppos=0xc06edf80) at /home/book/workspace/baohua/drivers/ch6/globalmem.c:65
65		unsigned long p = *ppos;
(gdb)disassemble
0xbf0001c0 <globalmem_read+0>:	mov	r12, sp
0xbf0001c4 <globalmem_read+4>:	push	{r4, r5, r6, r7, r8, r11, r12, lr, pc}
0xbf0001c8 <globalmem_read+8>:	sub	r11, r12, #4	; 0x4
0xbf0001cc <globalmem_read+12>:	ldr	r7, [r3]    ;可见,用第一种方法获取的地址做实验,断点的位置是正确的
0xbf0001d0 <globalmem_read+16>:	mov	r8, r3
0xbf0001d4 <globalmem_read+20>:	cmp	r7, #4096	; 0x1000
0xbf0001d8 <globalmem_read+24>:	mov	r12, r1
0xbf0001dc <globalmem_read+28>:	ldr	r0, [r0, #116]
0xbf0001e0 <globalmem_read+32>:	movcs	r6, #0	; 0x0
0xbf0001e4 <globalmem_read+36>:	bcs	0xbf00025c <globalmem_read+156>
0xbf0001e8 <globalmem_read+40>:	mov	r1, sp
0xbf0001ec <globalmem_read+44>:	bic	r3, r1, #8128	; 0x1fc0
0xbf0001f0 <globalmem_read+48>:	bic	r3, r3, #63	; 0x3f
0xbf0001f4 <globalmem_read+52>:	rsb	r5, r7, #4096	; 0x1000
0xbf0001f8 <globalmem_read+56>:	cmp	r5, r2
0xbf0001fc <globalmem_read+60>:	movcs	r5, r2
0xbf000200 <globalmem_read+64>:	ldr	r3, [r3, #8]  ;可见用第二种方法获取的地址做实验,断点位置已经偏离了正确值0x34个字节,但由于该地址是有效的地址,所以实际上还是可以正常调试的,并不会表现出显式的症状
0xbf000204 <globalmem_read+68>:	adds	r2, r12, r5
0xbf000208 <globalmem_read+72>:	sbcscc	r2, r2, r3
0xbf00020c <globalmem_read+76>:	movcc	r3, #0	; 0x0
0xbf000210 <globalmem_read+80>:	cmp	r3, #0	; 0x0
0xbf000214 <globalmem_read+84>:	movne	r0, r5

五、参考资料

  1)韦东山 《嵌入式linux视频教程_三期项目实战之USB摄像头监控》

  2)宋宝华 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》

  3)Linux 可加载内核模块剖析

  4)使用 KGDB 调试 Kernel On Red Hat Linux

  5)ARM64 的 Linux 内核 kgdb_kdb 调试 - 字节岛技术博客

  6)调试linux内核模块

  7)(经验证有问题)Linux下用GDB调试可加载模块

  8)ELF格式探析之三:sections

猜你喜欢

转载自www.cnblogs.com/normalmanzhao2003/p/11511063.html