cisen / blog

Time waits for no one.
133 stars 20 forks source link

详解LMA & VMA 【转】 #887

Open cisen opened 4 years ago

cisen commented 4 years ago

关于LMA和VMA,这个问题,有点点小复杂,不过,此处,我会把我的理解,尽量通过通俗的方式解释出来,以方便理解。当然,鄙人水平有限,难免有错,希望各位批评指正。

一般提及LMA和VMA,多数情况都是和ld,链接器相关的。 在了解这两个名词的详细含义之前,有些基本知识和前提要说一下: [基础知识] 1。从你写的源代码到执行你的程序,一般经历了这几个过程: 源代码编辑 -> 编译 -> 链接 -> 装载 -> 执行 2。编译,简单说就是用编译工具,将你的源码,变成可以执行的二进制代码,也叫做目标文件,当然只是对应某一种硬件平台,比如此处我用的是Intel的X86系列的CPU,编译出来的,就是针对X86的二进制代码。 3。链接就是,将多个目标文件合并为一个目标文件,称作可执行文件。 4。每个目标文件都包含一连串的section,最常见,最基础的至少有: .text,代码段,就是CPU要运行的指令代码; .data,数据段,程序中包含的一些数据,放在这个段里; .bss,未初始化段,记录了程序里有哪些未初始化的变量,就相当于只记录对应的名字,留着程序运行前去初始化为0,所以,此处并不占用具体空间。打个比方就是,只记录人名,没有人站在这里占地方,而对应的.text和.data段,都是既有人名(函数或者变量名),又占对应的地方(包含具体空间记录到底是什么指令代码和数据的数值是多少)。 5。section一般可以分为loadable与allocatable. 通俗点说就是: loadable,可加载,就是,原先目标文件里面包含对应的代码或数据,所以,装载器要把这些内容,load到对应的地址,以便程序可以运行; 而allocatable,可分配的,最简单理解就是上面提到的.bss段,那里记录了人名,到时候,你要给这些人名分配空间给你站的地方,对应着也就是变量所要占用的具体内存空间了。 其他还有既不是loadbale的,也不是allocatable的,比如只存储debug信息的段,此处不多解释。 [前提] 程序已经编译好了,有了一个可执行文件,也叫目标文件,二进制文件,才会有后面的把程序装载,运行的事情。

看完了基础知识和前提,再说我们此处的主题,才能更加清楚是咋回事: 对于目标文件中的loadable或allocatable的section,其都有两个地址:VMA 和 LMA 。 知道了其来由,再看具体解释:

[LMA 详解]

LMA的英文原版解释: LMA(Load Memory Address): the address at which the section will be loaded. 什么是Load Memory Address,内存装载地址呢? 此处,单单从名字上,我们就可以看出几层意思: 1。load,装载 为何要装载呢? 因为,如果想要使你的程序(即经历过,由你的源码,通过编译器的编译,链接器的链接,形成的那个可执行文件),能在内存里面运行,那么肯定涉及到一点,就是,有人,把你的这个程序, ,从此处常见的存储器硬盘里面,搬到内存里面去了,然后才有可能运行。而这里的装载,就是对应这个意思。就是把程序,从硬盘里面,装载Load,到内存里面去了。 对应地,放到内存哪里去了呢?就是LMA,Load Memory Address,就是把你的程序中的对应的内容,详细点说就是,把其中的.text代码段,.data数据段等内容,搬到,也就是copy拷贝到,内存的LMA地址处了。 2。Memory,内存 上面已经解释了,这里再多说几句。 程序运行的本质,就是CPU读取到指令,然后执行。这里就涉及到,如果想要你的程序运行, 首先,你应该把对应的指令,放到合适的地方,CPU 才能读到,才能执行。 此处合适的地方,有人想到,直接放到硬盘这里,CPU过来读取,然后执行不就可以了吗,还不用这么麻烦地将(指令)代码搬来搬去的,多省事。但是实际上,系统就是这么“笨”地搬来搬去,原因在于,从硬盘上直接读取指令,速度比直接从内存,一般PC 上是各种类型的RAM,比如DDR,此处统称为Memory/内存, 要慢很多倍,所以,系统才会不嫌弃麻烦,把代码拷贝到内存里面去,然后从内存里面读取指令,然后执行,这样效率会高很多。 所以,此处简单说就是,为了总体效率,对于普通系统,比如PC,程序的执行都是在Memory,内存里面执行的。

因此,用一句话总结就是: 代码从ROM被装载到RAM的某个地方,那个地方的地址,就是LMA 。

[VMA 详解]

英文解释: VMA(Virtual Memory Address):the address the section will have when the output file is run; 那啥是虚拟内存地址呢?简单说就是,你程序运行时候的所对应的地址。 此处所谓的虚拟,一般来说,指的是启用了MMU之后,才有了虚拟地址和实地址。 此处,我们可以简单的理解为,就是内存的实际地址即可。 程序运行前,要把程序的内容,拷贝到对应的内存地址处,然后才能运行的。 因此,一句话总结就是: 代码要运行的时候,此时对应的地址,就是VMA。

[理解此句:在多数情况下,LMA和VMA是相等的] 这句话,说白了,可以(武断地)这么理解: 如果是普通PC电脑,也就是上面说的,大多数情况下,那么LMA和VMA是一样的,也就是,程序被加载到内存的什么地方,也就在什么地方运行。 如果是嵌入式系统,也就是相对的“少数情况”,LMA和VMA不一样。而其中最常见的一种情况就是, 程序被放到ROM中,比如设置为只读的Nor Flash中,也就是LMA的地址是Nor Flash的地址, 此如随便举例为0x10000000,而程序要运行时候的地址是内存地址,比如0x30000000,也就是VMA 是0x30000000,这时候,就要我们自己保证,在程序运行之前,把自己的程序,从LMA=0x10000000拷贝到VMA=0x3000000处,然后程序才可以正常运行。

有人会问,反正对于ROM来说,CPU 也是可以直接从ROM里面读取代码,然后运行的。为何还要前面提到的,弄个LMA 和VMA不同,搬来搬去的呢?因为ROM,顾名思义,是只读的,只能读取,不能写入的。 而程序中的代码段,由于只是被读取,不涉及到修改写入,是没有问题的。但是对于数据段和bss位初始化段来说,里面的所有的程序的变量,多数都是在运行的时候,不仅要读取,而且要被修改成新的值,然后写入新的值的,所以,如果还是放到ROM里面,就没法修改写入了。 而且,另一个原因是,CPU从ROM,比如常见的Nor Flash中读取代码的速度,要远远小于从RAM,比如常见的SDRAM,中读取的速度,所以,才会牵扯到将代码烧写到ROM里面,然后代码的最开始,将此部分程序reaload,重载,也就是从此处的ROM的地址,即LMA,重新拷贝到SDRAM中去,也就是VMA的地方,然后从那里运行。

[后记] 关于LMA 和 VMA: Linker,链接器的作用: 1。将LMA写到(可执行的)二进制文件里面去 2。解析符号。即,把不同的符号,根据符号表中的信息,转换成对应的地址。此处只涉及VMA,即程序运行时候的地址。

Loader,装载器的作用: 1。从二进制文件中读出对应的段的信息,比如text,data,bss等段的信息, 将内容拷贝到对应的LMA的地址处。此谓,装载(对应内容)到装载地址(LMA)。 2。如果发现VMA!=LMA, 即 程序运行时候的地址,和刚刚把程序内容拷贝到的地址LMA,两者不一样, 那么就要把对应的内容,此处主要是data,数据段的内容,从刚刚装载到的位置,LMA处,拷贝到VMA处, 这样,程序运行的时候,才能够在执行的时候,找到对应的VMA处的变量,才能找到对应的值,程序才能正常运行。

cisen commented 4 years ago

Linker Script,LMA,VMA实例分析 https://www.cnblogs.com/blogernice/articles/9856216.html 以前在学ld的script时两个比较重要的概念,即指定一个输出section的lma和 vma(分别是load memory address和virtual memory address),vma的作用是很明显地,就是决定run time address嘛,但lma有什么用呢?恩,对运行在linux这样的操作系统上面的应用程序来说,是没什么用的,毕竟应用程序都是被'load'到虚拟 地址空间中。但是在嵌入式底层firmware, bootloader开发来说,这个关键字不再打酱油。

看下面这么一段代码,汇编:

.section .text  
_symbol_in_text:  
mov $1, %eax  
.section .data  
_symbol_in_data:  
.long 0x90909090  

简单得不能再简单了,定义了两个段,.text和.data,.text里面就一条指令,.data里面也只有一个字。

先编译成可重定向的.o目标文件:

gcc -o xxx.o -c xxx.S  

接着用ld将其链接成可运行文件,这里其实只有一个目标文件参与链接,所以说链接其实是不对的,ld只是做做重定向或者叫定址的工作而已,外加在elf文件中产生描述文件的segment和section的信息等。

链接时首先提供一个最简单的ld script:

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")  
OUTPUT_ARCH(i386)  
SECTIONS  
{  
        .text 0x5000                    : { *(.text) }  
        .data 0x8000                    : { *(.data) }  
}  

在上面这个ld script中只定义了vma;根据ld的规则:如果没有用AT指令定义lma的话,那么lma默认等于vma。

这 里为什么两个段(.data和.text)的vma不一样?试想在嵌入式系统中是不是会遇到这种情况,即Flash(Rom)空间较大,Ram空 间相对较小,于是我们只希望让数据装载进Ram空间,代码就直接运行在Flash(Rom)中。比如Flash(Rom)的起始地址0x5000,Ram 的起始地址0x8000,所以这两个段的vma就必须对应到相应Region的起始地址上。不然会怎么着?不是跑飞就是读写的数据找不到。

链接:

ld ./lma_vma.o -T ./lma.equal.vma.lds  

生成a.out可执行文件。

注 意,这个a.out是‘可执行’的elf文件。对于bootloader或者firmware来说,一般是直接把一个binary文件 ‘burn’到板子上的。把elf文件剥离成一个binary文件,在万能的gnu tool帮助下,非常简单,一个objcopy便可搞定:

objcopy -O binary ./a.out  

好了,用file看一下文件类型:

./a.out: COM executable for MS-DOS  

应该就是'bin'文件了。

且慢,还记得之前我们没有用AT指令指定两个段的lma么。那么,问题来了,我们先用ls -l看一下有什么异样:

[root@localhost ldscript_test]# ls -lh ./a.out  
-rwxr-xr-x 1 root root 13K 11-04 20:55 ./a.out  

文件足足有13k大小。别忘了,我们的源程序只有一条指令和一个32位的字,并且是纯数据的bin文件,为什么有这么大?

看看这个纯数据的文件里面有些啥,借助于hexdump,真相一目了然:

[root@localhost ldscript_test]# hexdump ./a.out  
0000000 01b8 0000 0000 0000 0000 0000 0000 0000  
0000010 0000 0000 0000 0000 0000 0000 0000 0000  
*  
0003000 9090 9090  

最 开始01b8应该就是mov $1, $eax的instruction code。而0x3000位置的90909090显然就是我们定义在数据段的字了。因为链接器脚本中没有用AT指令专门为两个段指定lma,所以其lma 与vma相等,两个段相差了0x3000 bytes的长度。.text段之前没有其他段了,所以最终的bin文件中一开始就是.text段的内容,虽然只有2个字节,但仍然要过0x3000 bytes才是.data段。中间那些未知数就填0了。

这样有什么问题呢?因为我们知道0x8000已经是Ram了,难道我们要将全局数据 烧到一断电内容就消失的Ram中?并且,Flash(Rom)和 Ram之间相隔的0x3000 bytes不一定就对应实际的存储区域(比如也在Flash中),有可能根本就是hole。那么‘烧’这些0下去有可能会造成问题。

我们希望的结果是,烧写的data和text都在Flash(Rom)中,运行后再将data自搬运到Ram中。最好bin文件中两个段紧挨着,保持文件尽可能小的size。

下面的ld script在定义.data段时增加了AT指令来描述其lma,这样表示.data段的lma紧接在.text段的后面:

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")  
OUTPUT_ARCH(i386)  
SECTIONS  
{  
        .text 0x5000           :  
        {  
                *(.text)  
        }  
        .data 0x8000                : AT(ADDR(.text) + SIZEOF(.text))  
        {  
                *(.data)  
        }  
}  

这次链接、objcopy后生成的a.out文件看一下:

[root@localhost ldscript_test]# hexdump ./a.out  
0000000 01b8 0000 9000 9090 0090  
0000009  

只有9个字节大小,里面的内容正好是一条指令加上后面的0x90909090(指令后面的两个0x00是为了4字节对齐.data的pad)

这个bin文件就可以放心的烧写到Flash(Rom)中去了。不过,将.data段搬运到Ram的代码还是得自己写的。更系统的学习ld script,请info ld。

youth7 commented 1 year ago

这个issue汇集了很多很好的文章,点赞

gamife commented 1 year ago

感谢分享!
但是我还有个疑问,对于嵌入式,a.out文件总共九个字节已经没有LMA和VMA的信息了,

  1. 烧录软件是根据软件自身设置写入到嵌入式设备对应地址的吧?没有用到LMA?
  2. 嵌入式裸机也没有Loader(装载器)吧,所以就算VMA!=LMA, 也没有人去把LMA的内容拷贝到VMA。

这样看下来, 似乎LMA和VMA都没有用到?

michael-etzkorn commented 1 year ago

所以就算VMA!=LMA, 也没有人去把LMA的内容拷贝到VMA。

在启动脚本.ld里你可以定义参数,LMA在.data外面,VMA在.data里面,通过lds把握VMA和LMA的地址如下:

    __LMA_DATA_START = .;  
    .data : 
    {
       __VMA_DATA_START = .;
       *(.data)
      __VMA_DATA_END = .;
     } > ram

在代码里可以用这个参数拷贝 (risc-v assembly):

    la t3, __LMA_DATA_START
    la t4, __VMA_DATA_START
    la t5, __VMA_DATA_END

    bge t4, t5, copy_data_from_lma_to_vma_end

copy_data_from_lma_to_vma:

    ld t6, 0(t3)
    sd t6, 0(t4)

    addi t3, t3, 8
    addi t4, t4, 8
    ble t4, t5, copy_data_from_lma_to_vma           

copy_data_from_lma_to_vma_end: