0

ARM在汇编代码中调用C函数


对于ARM体系来说,不同语言撰写的函数之间相互调用(mix calls)遵循的是 ATPCS(-Thumb Procedure Call Standard),ATPCS主要是定义了函数呼叫时参数的传递规则以及如何从函数返回,关于ATPCS的详细内容可以查看ADS1.2 Online Books ——Developer Guide的2.1节。这篇文档要讲的是 汇编代码中对C函数调用时如何进行参数的传递以及如何从C函数正确返回

不同于x86的参数传递规则,ATPCS建议函数的形参不超过4个,如果形参个数少于或等于4,则形参由R0,R1,R2,R3四个寄存器进行传递;若形参个数大于4,大于4的部分必须通过堆栈进行传递。

我们先讨论一下形参个数为4的情况.
实例1:
test_asm_args.asm
//——————————————————————————–
        IMPORT test_c_args ;声明test_c_args函数
        AREA TEST_ASM, CODE, READONLY
        EXPORT test_asm_args
test_asm_args
       STR lr, [sp, #-4]! 
;保存当前lr
        ldr r0,=0×10       
;参数 1
        ldr r1,=0×20        
;参数 2
        ldr r2,=0×30        
;参数 3
        ldr r3,=0×40       ;参数 4
        bl test_c_args      
;调用C函数
        LDR pc, [sp], #4  
;将lr装进pc(返回main函数) 
        END
test_c_args.c
//——————————————————————————–
void test_c_args(int a,int b,int c,int d)
{
        printk(“test_c_args:\n”);
        printk(“%0x %0x %0x %0x\n”,a,b,c,d);
}
main.c
//——————————————————————————–
int main()
{
     test_asm_args();
     for(;;);
}

程序从main函数开始执行,main调用了test_asm_args,test_asm_args调用了test_c_args,最后从test_asm_args返回main.
代码分别使用了汇编和C定义了两个函数,test_asm_args 和 test_c_args,test_asm_args调用了test_c_args,其参数的传递方式就是向R0~R3分别写入参数值,之后使用bl语句对test_c_args进行调用。其中值得注意的地方是用红色标记的语句,test_asm_args在调用test_c_args之前必须把当前的lr入栈,调用完test_c_args之后再把刚才保存在栈中的lr写回pc,这样才能返回到main函数中。


如果test_c_args的参数是8个呢?这种情况test_asm_args应该怎样传递参数呢?
实例2:
test_asm_args.asm
//——————————————————————————–
        IMPORT test_c_args ;声明test_c_args函数
        AREA TEST_ASM, CODE, READONLY
        EXPORT test_asm_args
test_asm_args
       STR lr, [sp, #-4]! 
;保存当前lr
       ldr r0,=0×1 ;参数 1
       ldr r1,=0×2 ;参数 2
       ldr r2,=0×3 ;参数 3
       ldr r3,=0×4 ;参数 4
      
 ldr r4,=0×8
       str r4,[sp,#-4]! ;参数 8 入栈
       ldr r4,=0×7
       str r4,[sp,#-4]! ;参数 7 入栈
       ldr r4,=0×6
       str r4,[sp,#-4]! ;参数 6 入栈
       ldr r4,=0×5
       str r4,[sp,#-4]! ;参数 5 入栈
       bl test_c_args_lots
       ADD sp, sp, #4     ;清除栈中参数 5,本语句执行完后sp指向 参数6 
       ADD sp, sp, #4     ;清除栈中参数 6,本语句执行完后sp指向 参数7
       ADD sp, sp, #4     ;清除栈中参数 7,本语句执行完后sp指向 参数8
       ADD sp, sp, #4     ;清除栈中参数 8,本语句执行完后sp指向 lr
       LDR pc, [sp],#4    
;将lr装进pc(返回main函数) 
        END
test_c_args.c
//——————————————————————————–
void test_c_args(int a,int b,int c,int d,int e,int f,int g,int h)
{
       printk(“test_c_args_lots:\n”);
       printk(“%0x %0x %0x %0x %0x %0x %0x %0x\n”,
              a,b,c,d,e,f,g,h);
}
main.c
//——————————————————————————–
int main()
{
     test_asm_args();
     for(;;);
}

这部分的代码和实例1的代码大部分是相同的,不同的地方是test_c_args的参数个数和test_asm_args的参数传递方式。
在test_asm_args中,参数1~参数4还是通过R0~R3进行传递,而参数5~参数8则是通过把其压入堆栈的方式进行传递,不过要注意这四个入栈参数的入栈顺序,是以参数8->参数7->参数6->参数5的顺序入栈的。
直到调用test_c_args之前,堆栈内容如下:
sp->+———-+
        |  参数5  |
       +———-+
        |  参数6  |
       +———-+
        |  参数7  |
       +———-+
        |  参数8  |
       +———-+
        |     lr      |
       +———-+
test_c_args执行返回后,则设置sp,对之前入栈的参数进行清除,最后将lr装入pc返回main函数,在执行 LDR pc, [sp],#4 指令之前堆栈内容如下:
       +———-+
        |  参数5  |
       +———-+
        |  参数6  |
       +———-+
        |  参数7  |
       +———-+
        |  参数8  |
sp->+———-+
        |     lr      |
       +———-+

2

RO段、RW段和ZI段


一直以来对于ARM体系中所描述的RO,RW和ZI数据存在似是而非的理解,这段时间对其仔细了解了一番,发现了一些规律,理解了一些以前书本上有的但是不理解的东西,我想应该有不少人也有和我同样的困惑,因此将我的一些关于RO,RW和ZI的理解写出来,希望能对大家有所帮助。
要了解RO,RW和ZI需要首先了解以下知识:
ARM程序的组成
此处所说的“ARM程序”是指在ARM系统中正在执行的程序,而非保存在ROM中的bin映像(image)文件,这一点清注意区别。
一个ARM程序包含3部分:,RW和ZI
RO是程序中的指令和常量
RW是程序中的已初始化变量
ZI是程序中的未初始化的变量
由以上3点说明可以理解为:
RO就是readonly,
RW就是read/write,
ZI就是zero
ARM映像文件的组成
所谓ARM映像文件就是指烧录到ROM中的bin文件,也成为image文件。以下用Image文件来称呼它。
Image文件包含了RO和RW数据。
之所以Image文件不包含ZI数据,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可。包含进去反而浪费存储空间。
Q:为什么Image中必须包含RO和RW?
A:因为RO中的指令和常量以及RW中初始化过的变量是不能像ZI那样“无中生有”的。
ARM程序的执行过程
从以上两点可以知道,烧录到ROM中的image文件与实际运行时的ARM程序之间并不是完全一样的。因此就有必要了解ARM程序是如何从ROM中的image到达实际运行状态的。
实际上,RO中的指令至少应该有这样的功能:
1. 将RW从ROM中搬到RAM中,因为RW是变量,变量不能存在ROM中。
2. 将ZI所在的RAM区域全部清零,因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零。ZI中也是变量,同理:变量不能存在ROM中
在程序运行的最初阶段,RO中的指令完成了这两项工作后C程序才能正常访问变量。否则只能运行不含变量的代码。
说了上面的可能还是有些迷糊,RO,RW和ZI到底是什么,下面我将给出几个例子,最直观的来说明RO,,ZI在C中是什么意思。
1; RO
看下面两段程序,他们之间差了一条语句,这条语句就是声明一个字符常量。因此按照我们之前说的,他们之间应该只会在RO数据中相差一个字节(字符常量为1)。
Prog1:
#include <stdio.h>
void main(void)
{
;
}
Prog2:
#include <stdio.h>
const char a = 5;
void main(void)
{
;
}
Prog1编译出来后的信息如下:
================================================================================
Code RO Data RW Data Data Debug
948 60 0 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1008 ( 0.98kB)
================================================================================
Prog2编译出来后的信息如下:
================================================================================
Code RO Data RW Data ZI Data Debug
948 61 0 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1009 ( 0.99kB)
Total RW Size(RW Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1009 ( 0.99kB)
================================================================================
以上两个程序编译出来后的信息可以看出:
Prog1和Prog2的RO包含了Code和RO Data两类数据。他们的唯一区别就是Prog2的RO Data比Prog1多了1个字节。这正和之前的推测一致。
如果增加的是一条指令而不是一个常量,则结果应该是Code数据大小有差别。
2; RW
同样再看两个程序,他们之间只相差一个“已初始化的变量”,按照之前所讲的,已初始化的变量应该是算在RW中的,所以两个程序之间应该是RW大小有区别。
Prog3:
#include <stdio.h>
void main(void)
{
;
}
Prog4:
#include <stdio.h>
char a = 5;
void main(void)
{
;
}
Prog3编译出来后的信息如下:
================================================================================
Code RO Data RW Data ZI Data Debug
948 60 0 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1008 ( 0.98kB)
================================================================================
Prog4编译出来后的信息如下:
================================================================================
Code RO Data RW Data ZI Data Debug
948 60 1 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 97 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1009 ( 0.99kB)
================================================================================
可以看出Prog3和Prog4之间确实只有RW Data之间相差了1个字节,这个字节正是被初始化过的一个字符型变量“a”所引起的。
3; ZI
再看两个程序,他们之间的差别是一个未初始化的变量“a”,从之前的了解中,应该可以推测,这两个程序之间应该只有ZI大小有差别。
Prog3:
#include <stdio.h>
void main(void)
{
;
}
Prog4:
#include <stdio.h>
char a;
void main(void)
{
;
}
Prog3编译出来后的信息如下:
================================================================================
Code RO Data RW Data ZI Data Debug
948 60 0 96 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 96 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1008 ( 0.98kB)
================================================================================
Prog4编译出来后的信息如下:
================================================================================
Code RO Data RW Data ZI Data Debug
948 60 0 97 0 Grand Totals
================================================================================
Total RO Size(Code + RO Data) 1008 ( 0.98kB)
Total RW Size(RW Data + ZI Data) 97 ( 0.09kB)
Total ROM Size(Code + RO Data + RW Data) 1008 ( 0.98kB)
================================================================================
编译的结果完全符合推测,只有ZI数据相差了1个字节。这个字节正是未初始化的一个字符型变量“a”所引起的。
注意:如果一个变量被初始化为0,则该变量的处理方法与未初始化华变量一样放在ZI区域。
即: C程序中,所有的未初始化变量都会被自动初始化为0。
总结:
1; C中的指令以及常量被编译后是RO类型数据。
2; C中的未被初始化或初始化为0的变量编译后是ZI类型数据。
3; C中的已被初始化成非0值的变量编译后市RW类型数据。
附:
程序的编译命令(假定C程序名为tst.c):
armcc -c -o tst.o tst.c
armlink -noremove -elf -nodebug -info totals -info sizes -map -list aa.map -o tst.elf tst.o
编译后的信息就在aa.map文件中。
ROM主要指:NAND Flash,Nor Flash
RAM主要指:PSRAM,SDRAM,SRAM,DDRAM

0

[转] 嵌入式单地址空间中实现动态加载的过程


转至:http://blog.csdn.net/pengzhenwanli/archive/2008/04/23/2319412.aspx#875603

之前有一篇文章是关于嵌入式单地址空间实现动态加载的想法,里面描述的是我根据相关资料进行猜测的地方,以及从技术上来说,可能需要的技术,最近难得有空闲时间,我实现了一下动态加载的。目前已经成功实现,下面说一下实现的过程。

先说一下实现此技术需要的平台:

OS

CPU7+cache

BasebandVT3406

ADS1.2

说一下这些东西的来历,Nucleus是实时嵌入式单地址空间操作系统,CPU是介于ARM7ARM9之间的CPUBB芯片是VIA出的,这些东西目前都已经收掉不再使用,我也正好离职,从而有时间去实现一下动态加载的问题。

从理论上来说,动态加载很简单,只需要把当前的PC指针指向下一句执行的语句即可。也就是使用如下的ASM就可以实现:

MOV PC, Address

       这样就可以顺利执行程序,在我实现的时候,考虑如下问题,程序执行如何返回,参数如何传递,程序执行完毕返回到哪里。

       这些问题的解决,看起来比较复杂,其实很简单,程序的返回是放在LR中,这样在上一个函数调用的时候,只要LR的值不变,这样可以在下一个函数调用的时候,同样使用LR,这样就可以顺利返回。关于参数传递,由于ARM中使用r0-r3传递参数,这样只要不更改r0-r3,就可以顺利传递参数。这个地方想明白,我用了好久,特别是返回地址的问题,程序如何执行,应该返回哪里。解决方法如下

       实现DynamicLoader(UINT8 *pAddress)

MOV PC, R0

这个地方一定要用ASM实现,否则无法完成需要的功能。由于在调用此函数时,已经把函数的返回地址放到LR中,具体ASM如下:

MOV r0,address

BL DynamicLoader

       由于DynamicLoaer的实现问题,没有实际的返回,也就是不需要使用

BX lr

       这样来做为函数的返回。这是由于R0所指向的一个函数的开始地址,从DynamicLoader开始,其实执行的是另一个函数,这里的DynamicLoader只是起到了一个跳转的作用。但是又必须使用函数调用的方式来进行,而不能直接跳转,否则函数没有办法返回。由于BL的时候填充了LR,这样在下一个由于实际调用不是使用的B指令,因为并没有设置LR,这样仍旧是在调用DynamicLoader时的LR,因此可以正确返回。

       函数可以正确调用并返回,这是程序很大的一个进步,这样就可以实际构造可以运行的程序了。

       下面说一下ADS编译为二进制可执行文件的问题,使用编译器如果一开始把所有的数据都放好,这样包括全局变量和静态变量,以及函数的执行地址等,都已在LINK的时候根据指定规则确定实际的运行地址,也就是说所有的函数的实际运行地址在LINK的时候已经确定。这样对于动态运行来说是不可行的,因为既然要动态加载,就需要所有地址都是静态的,因为每次对于读入内存的数据,起始地址是不缺定的,因此不能再LINK时把所有的地址固定死。

       解决这个问题有两种方式,一种是在scatter loader中把程序的可执行地址固定好,在LINK时不LINK实际的数据,而在系统启动的时候,把这部分可执行文件拷贝的固定的地址,这样可以作为一个整体运行。但是这种方式由问题,就是应用的大小什么的都是固定死的,不能太灵活,不能根据应用实际调整。

       这里使用另外一种方式,选择程序不在LINK的时候把所有的地址固定死,而是使用相对独立的函数调用方式。如下:

       原来的方式可能使这这样

       BL 0×10008;

       而使用相对的地址程序如下:

       ADD r5,pc,#18

       BL r5

       这样虽然多了一句,但是可以做到函数的运行地址是动态指定的,而不是编译为固定的地址。

       其实ADS提供了把函数编译为独立地址的方式,

       COMPILER使用如下的参数/ropi/rwpi

       LINK使用如下的参数-rwpi –ropi

       就可以把编译的程序做到运行时地址是独立的。

       从上面来开,编译时地址的问题,还有运行时加载的问题,都已经顺利解决。但是这里还有一个问题,就是如何确保动态应用如何每次在使用的时候,都从固定的入口进入的问题。也就是说,虽然有了内存中的运行地址,但是如何保证每次都从固定的函数开始执行呢?如果每次都从编译的可执行文件0地址开始执行,没有办法保证每次调用的是同一个函数。

       这个可以通过LINK来保证每次是同一个函数在0地址,使用如下的参数

-first DyanmicAppEntry

       这样就可以保证DyanmicAppEntry的入口地址为可执行文件的开始了。

       上面的文章解决了动态编译和加载的问题,下面说一下动态应用的问题。如果要使一个应用有价值,比然需要提供本地的功能调用,而且对于手机来说,系统已经基本上实现了大多数的功能,如果在动态应用中再重复实现一些功能,可以说既浪费了空间,又浪费了时间。而且对于硬件相关的功能,必须通过本地调用来进行,这样就需要如何把本地调用传入动态应用中。如上文所说,本地的调用地址都是在LINK的时候确定的没有办法直接在动态应用中使用,这样需要在运行时把本地调用传入动态应用,由于动态应用的入口还有好几个参数可以使用,这样就可以构造一张系统调用的表,在运行的时候传入动态应用,这样可以通过表来调用系统功能,这样就解决了系统本地调用的问题。

       这里要特别说一下安全的问题,由于动态应用是直接更改PC指针运行的,这样,如果应用出错,系统可能就CRASH了,无法再继续运行,而且由于可以调用本地系统调用,可能做许多意想不到的功能,这样就可以在系统调用的时候

增加一个中间层,一些核心功能,必须满足一定的权限在可以调用。

0

[转] 在嵌入式单地址空间OS中实现动态加载的问题


转至:http://blog.csdn.net/pengzhenwanli/archive/2007/02/26/1514689.aspx

本文的的主要想法是来源于手持设备可运行应用程序和如何实现智能机问题的思考。问题的主要来源是关于可扩充应用程序的考虑,目前大部分手机都是非智能机,也就是不能扩充应用程序

1.智能手机与非智能手机
        一般来说,智能手机目前公认三大系统,Windows Mobile,Linux和Symbian,也即是说采用这三种系统的手机,都成为智能机。我认为,从技术上来讲,智能机最主要的特征就是第一可扩充应用程序,也就是说用户可以自行安装需要的程序而不局限于手机自带的,第二就是多用户任务,也就是用户具有同时运行多个应用程序的权力。而非智能手机的操作系统可以说是五花八门,什么都有。但是基本上都有一个核心的特征,就是同时只能运行一个应用程序,而且所有的应用程序都运行在相同的地址空间,而智能机是运行在独立地址空间。一般来说,非智能机也支持多任务,不同的是这些任务共用相同的地址空间。用户操作的UI就是一个单独的任务,用户所能使用的功能基本上就是由UI提供的。
        我原来对于非智能机是非常的不屑,认为没有什么发展前途,但是我最近又弄了一个非智能机的手机用了一下,发现很多功能都非常的人性化,从使用上来讲,并不亚于智能机,由此引发了我对于非智机功能的思考。对于智能机来讲可扩充的应用程序一般来说,都是由第三方开发商开发的,稳定性都有所缺陷,并不如原生的系统应用程序稳定。而且有些比较好的用程序价格不菲,我见过使用智能机的人大部分都是使用破解的应用程序,我本人也是。这个行为是违法的,当市场成熟以后,比如像美国,是不太可能的。

        智能机由于操作系统功能强大,一般要求系统的硬件性能强劲,这样相应的功耗也大,待机时间相应的缩短。非智能机可以运行在性能较差的硬件上,并且获得的UI表现,不弱于智能机。这样就可以在相同的电力消耗的情况下,获得更长的使用时间。
2.非智能机获得智能机功能必须的要求
        如上文所说,只要非智能机实现智能机最主要的两个特征即可获得智能机的功能。一个是用户自行安装应用程序的功能。另一个是同时运行多个应用程序的功能。
3.用户自行安装应用程序的功能
        除了智能机以外,目前有两种技术都实现了用户可自行安装应用程序的功能,一个是BREW另一个是Java ME(J2ME)。下面分别说一下这两种技术,BREW技术由Qualcomm(高通)创建,包括一整套的体系,从运营到分发都有。在本文里只是对于客户端技术的说明,对于BREW技术而言,已经非常符合本文中所提及的技术,本文从另一方面来讲,也可以说是分析了BREW的基本技术。BREW技术高通把持的非常严密,目前而言,并没
有开放源代码而且对于技术内部也是进行严密的封锁,因此目前出现的文章都是猜测技术的实现,但是对于技术来讲,万变不离其宗,要实现某种技术,有些东西是绕不开的,就像使用CDMA技术,无论是WCDMA,CDMA2000,还是TDS-CDMA都无法绕开CDMA的基本专利一样。BREW技术基本达到了本文的技术要求,既能动态加载,也能同时运行多个任务,而且对于系统功能的使用不像JavaME一样有严重的限制,基本上来说,可以使用系统提供的所有功能。而对JavaME,所提供的功能十分有限,就连存取本地文件都不可以。对于系统功能的使用,如果没有附加的支持,基本上不可能,目前应用最得的是游戏,最多有些网络方面的应用。对于提供系统级别的应用,比如说闹钟等,根本无能为力。也许以后能够提供,但是本人不太看好。另外还有一个就是Flash Lite技术,这个技术我并没有接触,就不详细说了。对于BREW和JavaME我都有相当长时间的接触,了解也比较细致,有些问题还是能够说一下的。
        要解决用户可以自行安装应用程序的问题,必须解决以下几个问题,应用程序的加载运行问题,系统API的调用问题。
3.1应用程序加载
        对于嵌入系统来讲,与通常的Windows系统不一样,Windows的所用应用程序都在硬盘上,运行的时候根据需要加载到内存中,在运行,或者是使用虚拟内存技术,直接映射的硬盘也可以执行。而嵌入式系统通常是在ROM中,并不需要加载到内存才能运行,直接就可以运行,因此大部分的嵌入式系统都是统一做好一个系统的Image,然后放到ROM中运行。这样所有的地址都在编译期间确定,要是再动态加载应用程序,将会面临运行时地址确认的问题。一般而言,对于嵌入式系统,ROM使用Flash来代替,Flash中一部分作为ROM,另一部分作为嵌入式的文件系统,具体的系统格式这里不作考虑。要是可加载应用程序的话,一般来说是放在文件系统中。这样要运行可动态加载的应用程序,并不复杂,只要把应用程序调入内存中,运行时设置正确的寄存器就可以了。

这里也就是把可执行的文件加载到内存中,由于是单地址空间的,而不是像Windows一样每个应用程序都是独立的地址空间,这样应用程序可以从任意地址开始执行,这样载入内存以后,把当前的执行指针PC这为此内存地址即可。这也是单地址空间的程序可以执行的关键。

3.2系统API的提供

 要提供可以运行时call的API,在单地址空间可加载应用程序的约束条件下,可以使用的方法也非常的单调。一般来说,单地址空间的应用程序是统一编译和链接的,这样生成可执行文件以后,所有的地址已经固定了,这样如3.1所说,应用程序如果每个都调用相同的系统API,比如memset等,由于应用程序时独立加载的,这样由于在编译过程中已经把所有的地址确定,所以每个应用必须链接独立的lib,这样造成了空间浪费。如果只是单独的应用,不提供这一步,系统已经可以动态加载了。

但是目前是要求在嵌入式单地址空间实现,必须考虑空间的问题。因此必须解决系统库可以动态加载的问题。也就是库的地址不是在链接的时候确定。只要能够解决编译时的链接问题,就可以做到。

关于
4.同时运行多个应用程序的功能

0

[转] 嵌入式系统中的模块动态加载技术


摘要

提出一种适用于嵌入式系统的模块动态加载技术,设计实现简单,占用资源少,开销小,并且成功运用于DeltaOS.可提高系统的灵活性和扩属性.介招加载与动态链接的原理和应用情况,解释相关术语,描述基本设计思路:详细说明该技术的核心。即模块声明、调用库、两级重定位表,最后给出结论。

关键词

模块 动态加栽 嵌入式系统DeltaOS

引 言

随着电子技术的飞速发展,嵌人式设备应用越来越广泛,复杂度也越来越高。这使得硬件和软件设计比例发生了很大变化,软件开发的比重越来越大。然而传统嵌入式开发过程中需要将应用与操作系统编译链接成一个整体,然后下载到目标机上运行。如果在调试过程中发现问题,需要重新编链接然后重复下载运行的过程。这样的开发流程周期长而且繁琐,已经越来越不适应快速市场化的需要。

为了适应多样化的嵌入式应用和加快嵌入式系统的开发过程,除了需要可靠的基础平台软件的支持,如带有文件系统、网络协议栈的RTOS和配套的集成开发环境,更重要的是需要可以动态扩展的系统平台。近年来,新一代的嵌入式操作系统已经开始使用动态扩展技术:将基本系统(包括操作系统以及其他共享功能调用库)和应用程序开发分开处理,支持模块更新和动态加载技术。很多主流的传统嵌入式操作系统厂商,如windRiver、Green HilIs、Lynxworks,都推出了面向航空航天、基础通信设备等领域的高可靠、高性能的RTOS版本,支持应用和系统组件的动态加载和更新;而在消费电子领域,相关的操作系统厂商,如symbian、Palm、Microsoft,更是积极推出了具有相应功能的操作系统,在新一代移动设备上得到了广泛应用。

为了成为可动态扩展系统平台,大部分嵌入式操作系统需要使用动态加载技术。总的来说,动态加载是指应用或者系统在运行过程中需要使用某模块的服务,于是通过一系列预定的动作将指定模块加载到系统中,让调用者继续顺利工作。它实现的关键就是加载与动态链接技术。因为加载和动态链接互相依赖,关系紧密,所以将两者放在一起进行讨论。

1 加载与动态链接机制

加载主要负责将模块程序从二级存储设备(比如硬盘或者Flash)搬移到指定内存空间,并且将模块交由系统加载器统一管理。

程序链接分为静态链接、加载时链接和运行时链接。静态链接就是将程序和它运行所需的全部库链接成一个执行文件。它的优点是可以独立运行、速度快,但是它链接生成的代码尺寸比较大。加载时链接是指程序在编译链接时不会把它用到的库链接到执行程序中,而是在它被加载器加载时才解析执行文件,依次把用到的库装载到系统中让其运行。它的优点是程序本身代码量减小,但运行时程序占的内存并没有减小,同时增加了加载器的工作量。动态链接是加载时链接的进一步发展,它是指将库的加载过程延迟到程序运行时执行。这种方式不会给程序引入额外的代码,也不会增加加载器的开销,只有当应用真正使用某库时才会加载该库,减少了不必要的空间占用。它的缺点是可能会有一些运行开销。

嵌入式系统中动态加载和普通的动态链接概念类似,但是嵌入式系统中的加载链接器有其自身的特点:它是交叉加载,主机端做一部分工作,比如程序的重定位,执行文件的解析等等;而目标机端相对简单,主要做模块搜索定位和空间分配,以及指定物理地址或者映射虚拟地址让其运行。一部分嵌入式系统不支持虚拟内存,应用和内核共享存储空间。当系统加载了多个应用到系统中时,一般需要使用overlap技术来解决内存空间有限的问题,即是当多个应用的运行地址空间冲突时,加载器会冻结当前暂时不运行的应用,让新加载的应用使用指定的地址空间,PairnOS中就采用了这样的设计。对于支持虚拟内存的嵌入式系统,加载器的工作被大大简化,每个应用都有可以运行在同样的虚拟的空间,不需要加载器为其重定位或使用overlap技术,因此提高了工作效率。Vxworks6.O,WinCE都使用了这种设计。两种方式在不同的领域都有比较多的应用。

文中提出的模块动态加载技术是基于支持MMU(Memory Management Unit)的32位嵌入式操作系统,采用了加载与动态链接技术。使用该技术构建的嵌人式系统面向高端市场,特别是对系统可靠性、安全性要求很高的领域。在DeltaOS新一代高可靠的版本HAR(High Available Reliable system)的研发过程中,即成功地实现了基于该设计的加载器LambdaLoader,达到了预期的性能要求。

2 模块动态加载的设计

2.1 设计思路

首先定义一些概念:模块、目标程序、接口函数地址表和调用库(call Library)。

①模块,主要是指加载器加载的一个单位,并且这里模块的概念主要是强调它是为应用或者系统提供一系列服务的提供者。

②目标程序,是指模块的使用者。它可以是应用,也可以是另一个模块。

③接口函数地址表(文中也称之为模块重定位表),指在模块中有一个数组表,该数组表的内容是该模块对外提供的函数接口的地址。

④调用库,是供模块调用者链接使用的专有库。它与相关模块一一对应,将封装了的模块接口供目标程序使用。除此以外,它还有一个运行时才确定的模块重定位表地址指针和模块动态查找定位的代码。

如果在系统中要实现动态加载,首先需要一种模块定位机制,使得调用者能够在系统中动态定位需要的模块,其次是要能让模块与目标程序动态的关联在一起,协调工作。为了解决这些问题,需要一系列相关的设计:规定模块的声明方式;简化目标机端模块地址空间定位的工作;重定位表的机制等等。基于这样的设计,系统可以比较顺利地实现动态加载。模块动态加载的工作流程如图l所示。这里描述的主要是目标机端的工作。

 

2.2 模块的声明

模块首先要定义它的相关属性。这里使用模块声明文件来完成这个工作。模块声明文件中需要定义:模块名字、版本、对外提供的API接口。在系统编译模块程序后,会调用一系列的script代码。这些script会根据模块名字查找模块对应的模块声明文件,并根据该文件生成供模块调用者使用的调用库和与模块一起链接的附加库。

附加库包含系统后台通过调用script生成的接口函数地址表和模块注册函数。在每个模块的初始化函数中,会调用一个模块的注册函数(该函数主要工作是向系统注册模块的名字和接口函数地址表地址)。当模块被加载时,初始化函数会被系统调用,向系统注册模块信息,此后模块交由加载器统一管理。

2.3 调用库

每个模块在提供一个模块重定位表的同时,必须提供一个与之对应的模块调用库。别的目标程序必须并且只能通过调用库来使用这个模块提供的服务。每个调用库都有一个存储本模块重定位表的地址指针变量。该变量在模块被目标程序第一次使用时会被初始化为相应模块重定位表地址。

在模块第一次被目标程序使用即开始动态加载过程时,首先运行的是调用库的库初始化代码(Library initialcode),它通过指定的系统调用来初始化库中的模块重定位表基地址指针。此后每次目标程序使用模块提供的函数接口时,都通过以下公式得到该接口的实际地址:模块接口实际地址=模块重定位表基地址+函数index×4

在该公式中,函数index是指对应函数在模块重定位表中的数组下标值。因为根据模块声明文件生成的调用库中已经包含了每个函数的索引信息(index),同时在32位系统中需要乘以4得到准确的偏移量,所以当调用库中重定位表地址被初始化后,可以通过这样一个简单计算得到指定接口实际地址,完成函数调用。

当一个目标程序使用了模块,并正确动态加载后,其关系如图2所示。目标程序中链接了调用库,包含了函数跳转表和指向模块重定位表基地址的指针(ModuleBase);模块中则链接了附加库,包含了函数接口地址表(模块重定位表)。调用模块函数时,经过动态加载模块的过程以后,目标程序的模块重定位表基址指针指向了对应模块的函数接口表,然后函数调用就可以顺利进行了。

 


2.4 两级重定位表

在嵌入式领域,为了降低性能开销和增加确定性,目标机端加载器不会做程序重定位,而将相关工作在主机端完成,所以目标机端加载的所有程序都是绝对定位后的程序.为了实现系统动态扩展,必须使各个模块能够单独链接生成执行程序,并且运行时不用关心彼此的定位,这样即使一个模块被动态替换后也能同其他程序一起协调运行。这里通过两级重定位表机制来完成这个协调性的工作。

对于内核、操作系统组件模块或提供服务给其他目标程序使用的模块,要维护一张本模块提供的接口函数地址表(即模块重定位表,这里称之为二级重定位表)。为了保证本模块的向后兼容性,模块必须保证其接口函数在模块重定位表中的相对位置固定。即使今后不能提供这个接口函数,也需要将其保留,以保证同以前版本的二进制兼容性。

在模块的初始化代码中,模块通过系统调用向加载器注册这个模块重定位表的地址,注册时需提供模块名和模块重定位表的地址。加载器中管理着一个称为一级重定向表的表格。这个表的表项是“模块名”到“模块重定位表地址”的映射。因为这只是一个映射关系,所以各个模块对应的表项在一级表中的具体位置是可以改变的。

二级重定位表如图3所示。

 

使用两级重定位表的规则如下:

①模块可通过模块重定位表向其他目标程序提供接口函数;

②目标程序要使用别的模块提供的接口函数必须通过对应模块的调用库来实现;

③目标程序在使用别的模块提供的接口函数之前,必须通过加载器提供的系统调用服务获取对应模块重定位表来基地址初始化对方的调用库。

结 语

该设计实现了在嵌入式系统中的模块动态加载与更新,使得在嵌入式软件开发过程中,开发人员可以更有效的设计系统,共享资源,达到提高效率、产品快速市场化的目的。在基于DeltaOS的实现中,可以完成应用的任意加载卸载,系统组件的动态更新;多个应用可以共享一个全局的模块;一个应用可以同时使用多个模块等等。整个系统扩展性和灵活性大大提高,较好地满足了实际需要。但是设计中对容错性、健壮性的考虑还不够,在应用与模块的间接调用处理上还有优化的空间,所以在这些方面还需要进一步改进。

0

字节对齐详解(x86 + ARM)


一.什么是字节对齐,为什么要对齐?
   现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特 定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
   对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那 么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数 据。显然在读取效率上下降很多。

二.字节对齐对程序的影响:
   先让我们看几个例子吧(32bit,x86环境,gcc编译器):
设结构体如下定义:
struct A
{
   int a;
   char b;
   short c;
};
struct B
{
   char b;
   int a;
   short c;
};
现在已知32位机器上各种数据类型的长度如下:
char:1(有符号无符号同)    
short:2(有符号无符号同)    
int:4(有符号无符号同)    
long:4(有符号无符号同)    
float:4    double:8
那么上面两个结构大小如何呢?
结果是:
sizeof(strcut A)值为8
sizeof(struct B)的值却是12

结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个,B也一样;按理说A,B大小应该都是7
之所以出现上面的结果是因为编译器要对数据成员在空间上进行对齐。上面是按照编译器的默认设置进行对齐的结果,那么我们是不是可以改变编译器的这种默认对齐设置呢,当然可以.例如:
#pragma pack (2) /*指定按2字节对齐*/
struct C
{
   char b;
   int a;
   short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct C)值是8。
修改对齐值为1:
#pragma pack (1) /*指定按1字节对齐*/
struct D
{
   char b;
   int a;
   short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct D)值为7。
后面我们再讲解#pragma pack()的作用.

三.编译器是按照什么样的原则进行对齐的?
   先让我们看四个重要的基本概念:
1.数据类型自身的对齐值:
 对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
2.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
3.指定对齐值:#pragma pack (value)时的指定对齐值value。
4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
有 了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是 表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数 据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数 倍,结合下面例子理解)。这样就不能理解上面的几个例子的值了。
例子分析:
分析例子B;
struct B
{
   char b;
   int a;
   short c;
};
假 设B从地址空间0×0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定 对齐值4小,所以其有效对齐值为1,所以其存放地址0×0000符合0×0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4, 所以只能存放在起始地址为0×0004到0×0007这四个连续的字节空间中,复核0×0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为 2,所以有效对齐值也是2,可以存放在0×0008到0×0009这两个字节空间中,符合0×0008%2=0。所以从0×0000到0×0009存放的 都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0×0009到0×0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0×0000到0x000B 共有12个字节,sizeof(struct B)=12;其实如果就这一个就来说它已将满足字节对齐了, 因为它的起始地址是0,因此肯定是对齐的,之所以在后面补充2个字节,是因为编译器为了实现结构数组的存取效率,试想如果我们定义了一个结构B的数组,那 么第一个结构起始地址是0没有问题,但是第二个结构呢?按照数组的定义,数组中所有元素都是紧挨着的,如果我们不把结构的大小补充为4的整数倍,那么下一 个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐了,因此我们要把结构补充成有效对齐大小的整数倍.其实诸如:对于char型数据,其 自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,这些已有类型的自身对齐值也是基于数组考虑的,只 是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了.
同理,分析上面例子C:
#pragma pack (2) /*指定按2字节对齐*/
struct C
{
   char b;
   int a;
   short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
第 一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0×0000开始,那么b存放在0×0000,符合0×0000%1= 0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0×0002、0×0003、0×0004、0×0005四个连续 字节中,符合0×0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放
在0×0006、0×0007中,符合 0×0006%2=0。所以从0×0000到0×00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C 只占用0×0000到0×0007的八个字节。所以sizeof(struct C)=8.

四.如何修改编译器的默认对齐值?
1.在VC IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡Category的Code Generation选项的Struct Member Alignment中修改,默认是8字节。
2.在编码时,可以这样动态修改:#pragma pack .注意:是pragma而不是progma.

五.针对字节对齐,我们在编程中如何考虑?

   如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照 类型大小从小到大声明,尽量减少中间的填补空间.还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:有一种使用空间换时间做 法是显式的插入reserved成员:
        struct A{
          char a;
          char reserved[3];//使用空间换时间
          int b;
}

reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用.

六.字节对齐可能带来的隐患:
   代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:
unsigned int i = 0×12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;

p=&i;
*p=0×00;
p1=(unsigned short *)(p+1);
*p1=0×0000;
最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对齐的规定。
在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐.

七.如何查找与字节对齐方面的问题:
如果出现对齐或者赋值问题首先查看
1. 编译器的big little端设置
2. 看这种体系本身是否支持非对齐访问
3. 如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。

八.相关文章:转自http://.csdn.net/goodluckyxl/archive/2005/10/17/506827.aspx

ARM下的对齐处理
from DUI0067D_ADS1_2_CompLib

3.13 type  qulifiers

有部分摘自ARM编译器文档对齐部分

对齐的使用:
1.__align(num)
  这个用于修改最高级别对象的字节边界。在汇编中使用LDRD或者STRD时
  就要用到此命令__align(8)进行修饰限制。来保证数据对象是相应对齐。
  这个修饰对象的命令最大是8个字节限制,可以让2字节的对象进行4字节
  ,但是不能让4字节的对象2字节对齐。
  __align是存储类修改,他只修饰最高级类型对象不能用于结构或者函数对象。
 
2.__packed
 __packed是进行一字节对齐
 1.不能对packed的对象进行对齐
 2.所有对象的读写访问都进行非对齐访问
 3.float及包含float的结构联合及未用__packed的对象将不能字节对齐
 4.__packed对局部整形变量无影响
 5.强制由unpacked对象向packed对象转化是未定义,整形指针可以合法定
 义为packed。
    __packed int* p;  //__packed int 则没有意义
 6.对齐或非对齐读写访问带来问题
 __packed struct STRUCT_TEST
{
 char a;
 int b;
 char c;
}  ;    //定义如下结构此时b的起始地址一定是不对齐的
        //在栈中访问b可能有问题,因为栈上数据肯定是对齐访问[from CL]
//将下面变量定义成全局静态不在栈上
static char* p;
static struct STRUCT_TEST a;
void Main()
{
__packed int* q;  //此时定义成__packed来修饰当前q指向为非对齐的数据地址下面的访问则可以

p = (char*)&a;          
q = (int*)(p+1);      

*q = 0×87654321;
/*  
得到赋值的汇编指令很清楚
ldr      r5,0×20001590 ; = #0×12345678
[0xe1a00005]   mov      r0,r5
[0xeb0000b0]   bl       __rt_uwrite4  //在此处调用一个写4byte的操作函数
     
[0xe5c10000]   strb     r0,[r1,#0]   //函数进行4次strb操作然后返回保证了数据正确的访问
[0xe1a02420]   mov      r2,r0,lsr #8
[0xe5c12001]   strb     r2,[r1,#1]
[0xe1a02820]   mov      r2,r0,lsr #16
[0xe5c12002]   strb     r2,[r1,#2]
[0xe1a02c20]   mov      r2,r0,lsr #24
[0xe5c12003]   strb     r2,[r1,#3]
[0xe1a0f00e]   mov      pc,r14
*/

/*
如果q没有加__packed修饰则汇编出来指令是这样直接会导致奇地址处访问失败
[0xe59f2018]   ldr      r2,0×20001594 ; = #0×87654321
[0xe5812000]   str      r2,[r1,#0]
*/

//这样可以很清楚的看到非对齐访问是如何产生错误的
//以及如何消除非对齐访问带来问题
//也可以看到非对齐访问和对齐访问的指令差异导致效率问题
}

Random Posts Recent Comments

  • 女友糖尿病害我蛀牙 Says:

    汗一个…...

  • Htj06 Says:

    zhenyouchuangyi...

  • 电商圈 Says:

    试图该怎么建立啊,,怎在程序中是吸纳...

  • edward Says:

    看得人心旷神怡,好文,情不自禁的顶一下...

  • Daniel Says:

    我也在处理这个问题,没有找到好的方法。我用了楼上兄弟的方法,还是可以的。不知道您找到好的方法了吗、我暂时楼上兄弟的方法。...

  • 卡,卡 Says:

    弱弱问一句:博主,你博客的模板这样设计pv高吗?...

  • 站长工具 Says:

    博主,兔年快乐!...

  • health Says:

    great post!!I hope I can read more in your website....

  • pdu Says:

    好博文,支持分享...

  • 站长工具 Says:

    博主的文章很不错,我是站长工具-站长精灵的作者,一款专业的SEO工具软件(可以帮您提高博客的流量),想跟您交换个链接,不知可否...

Tag Cloud

arm audio blog brew cache class debug flash google html j2me java javascript Joke linux lua mobile mtk php python ror ruby server shell stream unix web windows 优化 动态加载 女人 女生 平台 开发 手机 技术 流媒体 测试 漫画 生活 男人 男生 缓存 芯片