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

发布时间:2024-07-11  

前言、写这篇文章的由来

      最近在学习韦东山嵌入式培训视频(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 %sn", 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%sn", 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

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。

我们与500+贴片厂合作,完美满足客户的定制需求。为品牌提供定制化的推广方案、专属产品特色页,多渠道推广,SEM/SEO精准营销以及与公众号的联合推广...详细>>

利用葫芦芯平台的卓越技术服务和新产品推广能力,原厂代理能轻松打入消费物联网(IOT)、信息与通信(ICT)、汽车及新能源汽车、工业自动化及工业物联网、装备及功率电子...详细>>

充分利用其强大的电子元器件采购流量,创新性地为这些物料提供了一个全新的窗口。我们的高效数字营销技术,不仅可以助你轻松识别与连接到需求方,更能够极大地提高“闲置物料”的处理能力,通过葫芦芯平台...详细>>

我们的目标很明确:构建一个全方位的半导体产业生态系统。成为一家全球领先的半导体互联网生态公司。目前,我们已成功打造了智能汽车、智能家居、大健康医疗、机器人和材料等五大生态领域。更为重要的是...详细>>

我们深知加工与定制类服务商的价值和重要性,因此,我们倾力为您提供最顶尖的营销资源。在我们的平台上,您可以直接接触到100万的研发工程师和采购工程师,以及10万的活跃客户群体...详细>>

凭借我们强大的专业流量和尖端的互联网数字营销技术,我们承诺为原厂提供免费的产品资料推广服务。无论是最新的资讯、技术动态还是创新产品,都可以通过我们的平台迅速传达给目标客户...详细>>

我们不止于将线索转化为潜在客户。葫芦芯平台致力于形成业务闭环,从引流、宣传到最终销售,全程跟进,确保每一个potential lead都得到妥善处理,从而大幅提高转化率。不仅如此...详细>>