关于OS与APP分离编译,怎样才能写“影子函数”骗过连接器。求高人解惑。。。
很早就想在arm7上做os和app分离的操作系统。一直没有实现。设想是这样的。
os集成了驱动函数,app另外编译,可以下载到flash的固定区域,类似bootloader的功能。和bootloader不一样的地方是:APP可以调用OS的驱动函数。
我想了好长时间只想到一种方法,就是软中断,这样做出来就像BIOS差不多。
昨天liuweiele Liuwei说有好几种方法,不必用软中断,只要用“影子函数”骗过编译器。
不过影子函数应该怎么编写呢?不可能手工一个一个的计算函数的位置吧,这样修改一次驱动那就要大动干戈。
求高手解惑 跟我之前提的问题类似。同问。 回复【1楼】yisea123
-----------------------------------------------------------------------
就是昨天看了你的贴子,又想起这档子事来了。
搞了好长时间,找了许多资料,也没找出个什么好办法来。 这方面的东西很少有参考资料,只能靠自己去研究和领悟. 1. 编译OS时给出对应函数的地址,然后编译应用时调用这个绝对地址.
2. 编译应用时编译成与地址无关的.由OS去加载应用并确定地址.
小型的系统上面这两个比较可行. 可以参考一下 windows 下的 dll 工作原理. 这个主要是看你要达到什么目的(比如是直接要求在ROM中运行,还是动态加载),以及你的OS的功能。比如如果你的OS支持动态加载(如VxWorks),那么将OS调用链接为外部函数就行了。
然而如果像uC/OS-II之类没有App加载功能的呢?那么你需要明白,首先OS部分要能够调用App,那么肯定需要知道它的入口地址,其次App需要调用OS功能,那么它必须有机制知道OS系统调用的位置。最简单的方式就是使用函数指针,那么App需要有个约定,比如将OS调用的位置以参数传入。这样启动OS后,其调用App的入口例程(显然App必须通过链接脚本将其定位到“特定位置”),将OS调用的信息通过参数export给App(如将所有要导出给App的函数,使用结构的数组定义,将数组的指针传递过去就是了),后续就OK了。
其实你的OS+驱动部分就是提供给App的系统调用,你要做的就是如何将你的系统调用的位置信息传递给App。具体实现嘛根据自己的需要做。关键是要控制好OS和App的链接,分配好各自的ROM和RAM区域。 研究一下rt-thread的模块加载部分代码,也许会有帮助,也是采用动态链接 我提供一个方法,不知道是属于哪一种类型,但绝对不是软中断。
1、如果OS和App都用C++编写,那就简单多了。将OS需要调用的函数全部放到一个类中去,并使用public声明,并进行实现。App中包含这个类的声明,直接定义一个指针指向OS中的类实现,调用之,完毕。
2、如果不使用C++编写,那就模拟上述方法。定义一个结构体,结构体的每一个成员都是一个函数指针。在OS中定义这个结构体的一个常量,在App中外部引用这个结构体,调用之。和1相比,这个过程比较麻烦一些,而且函数调用跳转的次数也多一些:App->函数指针->OS函数调用,比1的App->OS函数调用要多一道手续。
一般情况下很少有将OS封装成C++的,那就只能用2了,目前我就是这样用的,但是要注意,OS和App的ROM、RAM不能重叠。 MARK 有谁能给个实例。
这个最能说明问题。 #define RTM_EXPORT(symbol) \
const char __rtmsym_##symbol##_name[] = #symbol; \
const struct rt_module_symtab __rtmsym_##symbol SECTION("RTMSymTab")= \
{ \
(void *)&symbol, \
__rtmsym_##symbol##_name \
};
struct rt_module_symtab
{
void* addr;
const char* name;
}; 这是rt-thread里面的一段代码
把所有的系统API都链接到RTMSymTab段,这样就相当于有了一个OS的API的MAP文件,剩下的就可以仿照linux的系统调用了 回复【楼主位】hzr0071
-----------------------------------------------------------------------
我这边有这方面的方案,有空可以交流交流。Q 397125879 回复【12楼】yfpbird
-----------------------------------------------------------------------
能详细说说吗,俺对这个也很感兴趣! 回复【11楼】s200661524
-----------------------------------------------------------------------
LINUX系统调用软中断,难道也要此种方法吗? 这么多回复。。。。。。。没想到哎。
现在的问题是APP调用OS的时候需要地址,而编译的时候怎么才能取得OS的MAP,然后构造一个和OS结构相同,但是里面只有标号,没有函数的(或者函数尽可能的小)的东西骗过连接器进行连接。
昨天看到elf格式的文件里面有MAP的描述,这两天看一下。 回复【15楼】clingos
-----------------------------------------------------------------------
linux也把API导出了,去看看vmlinux.lds.h这个文件吧。 _UsrFunctionAddr =(int const *) 0x0000102c;
INT16U Crypt(INT8U keyType,INT8U *S,INT8U *D)
{
INT16U (* F_entry)(INT8U keyType,INT8U *S,INT8U *D);
F_entry =(INT16U(*)())(_UsrFunctionAddr);
return (*F_entry)(keyType,S,D);
}
_UsrFunctionAddr 对应OS中各个函数列表的指针。 可以考虑用SWI软中断,封装所有库函数。
OS侧,SWI就是一个查找表,维护起来比较容易;
APP侧,和一般的函数调用没区别,指令级的区别就是BX指令变成了SWI指令。
整个向量表由OS进行维护。其实也就是IRQ,FIQ两个向量需要动态处理。
要么用户用SWI注_册对应的函数,要么直接用VIA的向量中断,绕过OS。
和函数指针没本质区别,类似功能都是这么实现的。 围观,顶起来~~~~~~· 刚刚让ads编译一个小程序输出文本信息,信息如下
1$m 0x00000003 LcAbs --
2$a 0x32000000 Lc 1Code
3$d 0x320000b0 Lc 1Data
4G:\FL2440\实验指导&demo\ADS基础实验\实验源码\1.3-led\Init.s
0x00000000 LcAbsFile
5restart 0x32000000 Lc 1Sect0x8
6G:\FL2440\实验指导&demo\ADS基础实验\实验源码\1.3-led\led.c
0x00000000 LcAbsFile
7.text 0x32000008 Lc 1Sect
8BuildAttributes$$ARM_ISAv4$M$PE$A:L22$X:L11$S22$~IW$USESV6$~STKCKD$USESV7$~SHL$OSPACE
0x00000000 GbAbs --
9Delay 0x32000008 Gb 1Code0x34
10ledMain 0x3200003c Gb 1Code0x74
这个不知道会有什么用处。
有这个信息的话,倒是能手工在特定地址写出空函数来,然后下载的时候只提取.text段下载进去。
PS:怎么在特定位置写函数来着,汇编就行,就像51中org之类的语句。?????如果只用分散加载的话,什么时候才能写完加载文件啊。 今天pzq给我说iar里面有个功能是生成一个特殊的elf,既函数全是镜像,而非真正的代码。
下面是iar手册的原话
http://cache.amobbs.com/bbs_upload782111/files_45/ourdev_675668D5OJF9.jpg
(原文件名:QQ截图未命名.jpg)
大家有知道这个怎么用的么? 还可以参考下luminary lm3s9b96的rom函数的调用方式——和地址无关。 回复【23楼】lryylryy 性博士
还可以参考下luminary lm3s9b96的rom函数的调用方式——和地址无关。
-----------------------------------------------------------------------
能否直接上传代码看看呢?这个芯片没接触过。 9b96的rom内带一个safeRTOS系统和外设驱动API,使用方法和下载见TI官网。你把OS和驱动API放在flash一块位置(比如末尾),然后照它的方式建立调用规则,就可以达到一样的效果。
http://focus.ti.com/mcu/docs/mculuminaryfamilynode.tsp?sectionId=95&tabId=2597&familyId=1756&docCategoryId=10&viewType=mostrecent 如果使用gnu的那一套工具的话,得到相关的地址就会显得很容易,你可以直接在链接脚本里定义一个变量指定相关的地址就行了,在os和app代码里是能够直接使用链接脚本里的变量的 编译成静态库文件 回复【25楼】lryylryy 性博士
9b96的rom内带一个safertos系统和外设驱动api,使用方法和下载见ti官网。你把os和驱动api放在flash一块位置(比如末尾),然后照它的方式建立调用规则,就可以达到一样的效果。
http://focus.ti.com/mcu/docs/mculuminaryfamilynode.tsp?sectionid=95&tabid=2597&familyid=1756&doccategoryid=10&viewtype=mostrecent
-----------------------------------------------------------------------
谢谢,看看先。 真是好东西啊 大家顶起来 想到了一个方法不知道可不可行,大家给点意见:主要是基于软中断实现的
1)在OS编译的时候将所有的系统API都输出到一个段里面,可以在链接文件中确定以及源代码中用编译器宏定义来实现,比如我上面说到的在RTT中用RTM_EXPORT(symbol)来实现,这里面可以不用包含API的名字。为了保证版本的兼容性,这个顺序必须是固定的,也就是以后新添加的API必须加在这个段的后面而不能查到中间。假设系统API被脚本文件定义在 _api_start到_api_end之间
2)处理OS侧的软中断向量(ARM处理器就是SWI)。即编写SWI中断函数,主要功能是提取中断号根据中断号调用相应的API,因为上面在链接脚本已经定义了_api_start这个标号,在这里可以直接用,比如提取的SWI中断号为3,就直接调用_api_start()(当然不同的API还有参数问题,所以还要提取栈中的参数,为了简化这里暂且忽略这个问题)经过这两步OS侧就搞定了
3)中间层:类似liuweiele Liuwei所说的SDK,给OS包装一层外衣供APP调用,这里必须结合OS的侧该系统API提供一个包装,假设OS侧API有一个系统函数为system_add(int x,int y),为API表中的第5个位置,则可以给该函数包装的伪代码如下
add(int x,int y)
{
参数处理;
SWI 5
}
同理把所有的API都给封装好做成一个中间层SDK。
4)APP侧:包含上述SDK,在应用程序中再去调用add函数。
============处理完毕========
因为对编译器参数传递没怎么研究所以还没想清楚这些细节,中间参数的处理和函数返回值都给忽略了望各位大侠补充
这样处理的话OS侧API所在的物理地址对用户来说是完全透明的,不需要用户关心,也不需要用重定位。 学习了,看高手讨论~ 函数的代码,一般放在Flash,那么我们只需知道它在Flash的位置就可以执行函数了,至于使用软中断调度,这只是一个权限问题,如果利用 SWI 1 , SWI 2,SWI 3, SWI 4,这无疑是把函数的地址放在在内存表,或者是利用ID号一个个去判断,散装出函数地址,这样无疑加重了内存资源的利用。我的方案是:将函数地址放在norflash中的一个固定区域,对这个区域利用MMU来管理,可以设置此区域的权限是用户模式,那么当你访问此区域的数据用户就无需担心不能访问,把你访问的数据转换成实际的函数指针 运行就OK了,我先在的Hardwarw、OS、GUI、堆、APP、字库,都是使用这种方法,速度很快,并且减少内存的占用,但是牺牲的是Flash的资源,一片norflash都是在1M以上,你提供给应用的函数区间也很充分 回复【32楼】fengyuganyu
-----------------------------------------------------------------------
确实你这种方法对资源占用很少,访问速度快,但是我想既然楼主想搞出一个OS和APP分开编译的构架,那么目标CPU本身的内存和CPU处理性能应该已经不是普通的单片机可比的,也不用过多的考虑,否则也不会去弄这么复杂的动态加载机制,完全可以一起编译。以前也想到过你这个方案,但是有一个问题没解决,就是app侧需要知道OS侧的函数地址(不管怎么方法都必须以一定的方式提供给app),不知道你是怎么解决这个问题的。上面说的软中断可以很好解决这个问题,只要系统API在输出段的存储顺序不变,不管OS作了如何改动,应用程序对此不敏感,连中间SDK层都不用改 回复【33楼】s200661524
-----------------------------------------------------------------------
用软中断或者模拟软中断的好处显而易见。不过听说有人做出了不用软中断而是直接地址跳转的方法。
关键在连接器上,要么自己写一个连接器(达到这种水平的人太少),要么就用一个影子文件骗过连接器。
pzq给我说了iar工具的方法,不过还没有找到做出例子的具体方法(重新编写bootloader以下载程序,其中包括elf的识别和定位)。
如果能有人写出像uclinux用的elf的连接器就好了。
ps:如果能找到方法,或许能做出匹敌uclinux的系统也说不定。。。。。 回复【34楼】hzr0071
-----------------------------------------------------------------------
加载ELF没什么难点.可行性是确定的,剩下的只是工作量.
如果内存上M还好.如果在RAM只有几十K内存的系统上面.
想把APP烧在FLASH上面,但没法保证每次向系统申请的内存地址都一样,谁有好办法. 可以看看Contiki是怎么实现加载ELF文件的 回复【33楼】s200661524
-----------------------------------------------------------------------
你说的问题很好解决,没有难点,只需把OS侧的函数地址存到固定flash地址上,而你在APP映射头文件里面定义函数的地址就OK了,我现在的工程就是这样,是需要利用Flash的空间来替代内存空间,我先在的工程就是这样极易维护,底层和应用可以分开,不需要全部编译 要不这样,在OS启动过程中增加一个函数,把提供的功能函数地址按照顺序填到RAM前端(顺序必须固定)。
而App则定义一个指向这块RAM的结构体,里面就是函数地址…… 回复【38楼】fengxin32
-----------------------------------------------------------------------
你说的原理和我的差不多,只是你是在利用内存来存储函数地址,我是用Flash来存地址,异曲同工 回复【37楼】fengyuganyu
-----------------------------------------------------------------------
嗯指针确实可以实现这个功能,以前总想着可以把API地址提出来,却没想到把这个存贮地址位置也固定下来。也是这几天抱着linux的书肯才慢慢虑清楚软中断的实现方法。另外思维里面还保留这一些存储器保护的东西,用户模式不能去直接访问系统地址,否则别人的app里面乱用指针函数乱指就麻烦了,所以需要转换到特权模式去访问这些资源。以前仿照linux0.11改成一个小操作系统,有基本框架可以运行,也试着实现动态加载,但是没什么成果,看到这个帖子就想在接着做做,现在想把我的那个方案用上去试试,看看效果怎么样。另外我碰到的一个问题就是虚拟存储没解决,我的那个系统是完全按照早期linux设计的 每个任务固定占用32M的内存空间,切换的时候直接用MMU的快速上下文切换功能,页表也省了切换。但是这样一来就不能实现任务隔离了,也就是一个任务可以访问到另外一个任务的内存空间。但是linux的进程页表部分没看明白,有哪位大侠帮解释一下吗 回复【39楼】fengyuganyu
回复【38楼】fengxin32
-----------------------------------------------------------------------
你说的原理和我的差不多,只是你是在利用内存来存储函数地址,我是用flash来存地址,异曲同工
-----------------------------------------------------------------------
听了以上各位的讨论很有启发。
既然这个方法可行,能否做个最简单的DEMO例子提供给大家,方便大家学习呢? 回复【41楼】yisea123
-----------------------------------------------------------------------
这个和LZ的意思不是一回事,这样只是把函数固定了,就像LM的MCU一样,
LZ想是是APP可以分享编译,一个应用就一个APP,多个就多个APP,多人开
发,形成单独的bin,然后由固化的操作系统来运行!
目前我的想法是,首先OS和所有的驱动单独一个工程,把OS相关的服务函数
固化在ROM区,
APP的和代码也单独工程,但是这个APP的启动代码需要自己完成,比如ZI段
的初始化,RW段的搬运等,这样是为了避免编译器编译时增加的启动代码破坏
堆栈,其实也是让含有main的函数像正常的函数一样,不需要运行时的环境
初始化操作。
注意每个APP的地址空间不同! 一步一步来,先实现OS层只支持一个APP。
楼上哪位愿意花30分钟来写个DEMO,节省大家可能3天,3个礼拜的时间? 我先来个我今天刚写的,基于软中断,还没有没有验证
1)OS侧:添加SWI中断处理函数,主要代码如下
_start:
ldr pc,=reset
ldr pc,=undefined_instruction
ldr pc,=software_interrupt
ldr pc,=prefetch_abort
ldr pc,=data_abort
ldr pc,=reserved
ldr pc,=irq
ldr pc,=fiq
......................................
software_interrupt:
get_swi_stack
swi_save_user_registers
ldr lr,=ret
ldr pc,=do_system_call
ret: swi_load_user_registers
......................................
.macro get_swi_stack
ldr sp, =SVC_STACK_BASE
.endm
.macro swi_save_user_registers
sub sp, sp,#SVC_STACK_FRAME_SIZE
stmia sp, {r0-r12}
add r8, sp, #52
stmia r8!, {sp,lr}^
str lr,
mrs r6, spsr
str r6,
mov r0, sp//至此r0可以作为参数指针传递,后面可以看到系统API都是一个结构体类型的参数,而R0即为结构体首地址
.endm
.macro swi_load_user_registers
ldmia sp, {r0-lr}^
ldr lr,
add sp, sp,#SVC_STACK_FRAME_SIZE
movs pc, lr
.endm
....................................................
void do_system_call(struct cpu_registers *regs)
{
unsigned int callnum;
callnum = regs->r0
fun_ptr fun; //函数指针
fun=sys_call_table;
regs->r0=(*fun)(regs);//返回值
}
......................................................
typedef int(* fun_ptr)(struct cpu_registers *regs);
fun_ptr sys_call_table[] = {
sys_hello,sys_fork
};
int sys_hello(struct cpu_registers *regs)
{
printk("hello word\n");
return 0;
}
int sys_fork(struct cpu_registers *regs)
{
printk("fork\n");
return 0;
}
..........................................
...........................................
中间层SDK:
#define NR_hello 0
#define NR_fork1
定义无参数函数调用形式type:返回值类型,name 函数名
#define system_call0(type,name) \
type name()\
{\
unsigned int res;\
unsigned int fun_id=NR_##name;\
asm volatile(\
"ldr r0,%1 \n\t"\
"swi \n\t"\
"str r0,%0 \n\t"\
:"=m"(res)\
:"m"(fun_id)\
:"r0","memory"\
);\
return (type)res;\
}
定义1个参数函数调用形式。type 返回值类型,name函数名,atype:参数a类型,a参数值
#define system_call1(type,name,atype,a) \
type name(atype a)\
{\
unsigned int res;\
unsigned int fun_id=NR_##name;\
asm volatile(\
"ldr r0,%1 \n\t"\
"ldr r1,%2 \n\t"\
"swi \n\t"\
"str r0,%0 \n\t"\
:"=m"(res)\
:"m"(fun_id),"m"(a)\
:"r0","r1","memory"\
);\
return (type)res;\
}
....................................
...................................
APP侧调用 system_call0(int,hello)
system_call0(int,fork)
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
完毕 Mark,留待以后学习! 回复【45楼】lanxing18
mark,留待以后学习!
----------------------------------------------------------------------- 绝对地址定位的函数库文件,很久以前玩过,后来忘了怎么搞,大部分编译器都支持。
os么,玩玩就行了,不要当真 回复【40楼】s200661524
-----------------------------------------------------------------------
你所说的基于Linux0.11的OS框架,早前我也看过,但是其实际意义不大,它是利用了MMU的快速切换,省去代码的切换,至于你所说的任务不隔离不太懂你的的意思,你大概是在说如何防止内存越界,其实这个实现起来也简单,你可以看下Linux里面段错误是怎么产生的,就知道怎么解决了。以这个OS的循环调度算法我比较欣赏,但是实际调度机制在大多情况下不可取。 回复【48楼】fengyuganyu
-----------------------------------------------------------------------
linux0.11没有用到快速切换,一直到1.0的也没有用到,我说的是arm体系的MMU里面FCSE实现快速切换,另外0.11每个任务虚拟空间是64M,而FCSE是32M,所以为了利用这个功能我特意调整到了32M。学习操作系统的思想么,又不是在系统里用,所以没什么意义大不大的问题,看个人兴趣而且早期版本还没有arm体系的,都是基于X86的,想下个arm的还真没搜到,我也是结合一本书讲这个内核的书慢慢修改到arm上的,删了很多功能模块,基本把这个版本的内核看玩了,但是要说看2.6的内核现在还真没那个实力。早期版本还是只有X86体系的,它是改变全局描述表寄存器和局部描述表寄存器来实现切换的(应该是段式存储管理特有的)
关于第二个问题我说说我的疑惑的地方
关于访问控制可以设置进程私有空间页目录权限为用户级,特权级可读写,内核空间页目录设置为特权级可读写。这样每个任务都有自己的页目录,而存储进程所控制的4G虚拟空间一级页目录有两种方法:
第一种是每个进程都存储,但是这样每个进程至少得16K内存来保存一级页目录(包括1G内核空间页目录)。
第二种:不保存所有的页目录,只保存自己用到的,比如最开始进程使用了自己私有空间最低1M空间和最高1M空间,那么进程只需要保存2个页目录项,另外内核空间1G页目录是公用的可以不保存。
那么这两种方法的话进程切换过程是不一样的。第一种方法只需要将相应进程的页目录基址写到CP15里面的页表基址寄存器,然后刷新TLB即可;而第二种方法需要在内存中存储一个全局的页目录(4096项共16K),初始化的时候CO15页表基址寄存器指向这个全局页目录基址。进程切换的时候只需要将上次进程的页目录的用户空间(0-3G)部分页目录清空以防止被新进程访问到,然后将新进程用到的页目录写上去,刷新TLB即可,也就是第二种方法不用改变CP15的页表基址寄存器。
现在对内核到底使用哪种方法没搞明白,或者关于这块我的理解有误,请大家指点一下疑惑,非常谢谢!
另外看到X86结构关于虚拟内存部分的寄存器比较多,除了全局基址寄存器CR3之外,还有tr和ldtr,idtr,早期的X86结构用的是段式内存管理,没用到分页试的,虽然说也支持分页,但是好像1.0以前源码中没有使用,跟arm体系的处理有点不一样。
修改:今天去追踪源码,进程切换使用到了switch_mm->cpu_switch_mm->cpu_set_pgd,貌似是第一种方法,直接去修改CP15的页表基址寄存器? 从尽量小的切换开销考虑,切换页表基址寄存器应该是最为通用并且开销较小的方式。
对于Linux这种通用目的设计的系统而言,采用别的方法的可能行不高。
对于在MMU之后的PA地址Cache,切Context连刷Cache都不用。我怀疑这是ARM后来把Cache从MVA改成PA的主要原因。 表示关注
MTK就是这样搞的,最近没时间研究 仿照EXE文件格式,加载呢? 回复【50楼】dr2001
-----------------------------------------------------------------------
不是刷指令catch和数据catch,是tlb页表catch,这个肯定还是要刷的任务不同页表也换了。 不懂 mark 回复【52楼】lileistone 三块石头
-----------------------------------------------------------------------
如果使用静态库+软中断这种方式比较简单,因为没有涉及到动态链接过程,所以也没有重定位问题,只需要开辟新进程建立页表映射然后加载文件到编译的时候的基地址上就可以了,其他方法没想到。
另外关于页目录,又跟踪了一下源码,差不多能确定是每个新进程都会有一个页目录,看到两个步骤,第一个是复制内核页目录还有一个函数中复制了用户空间页目录 回复【楼主位】hzr0071
-----------------------------------------------------------------------
在我的操作系统上,应用程序客户端使用异常捕获/软件中断方式进行系统调用;
API对系统调用进行了封装,使得应用程序可以独立编译.
http://cache.amobbs.com/bbs_upload782111/files_46/ourdev_678541ISSEYM.JPG
(原文件名:系统调用_软件中断方式.JPG)
http://cache.amobbs.com/bbs_upload782111/files_46/ourdev_678542V5LGI2.JPG
(原文件名:系统调用_异常捕获方式.JPG) liuweiele 厉害 要是能开放源码参考参考就好了 那怕就一部分 膜拜 回复【56楼】liuweieleLiuwei
-----------------------------------------------------------------------
您好,当时也是想搞一个OS与APP分别编译的系统,刚好搜到您的帖子了,拜莫中,软中断这种方式能想通,也在自己的系统上实现了几个简单的调用,但是您说的“应用程序客户端使用异常捕获/软件中断方式进行系统调用”,其中“异常捕捉”是什么意思呢?能从用户模式访问到受系统保护的资源我能想到的只有SWI指令切换到管理模式,这也是用户模式能使用的“唯一”手段,这里“唯一”要打上引号,因为IRQ,FIQ中断这是异步信号是硬件直接控制的,所以用户没法去控制,虽然有些CPU可以通过寄存器软件引起中断,但是CPU硬件资源在有存储保护的系统里面是严格控制的,用户不可能有权限操作。还有一些异常,如未定义指令,我想如果去用这种方式的话还不如SWI方便,终止模式也不可能子在用户模式去用。现在的水平所能想到的就是通过中间层去包装OS的系统调用构成一个库文件,有系统调用的地方就使用SWI方式去实现,如同C库一样提供给用户,其中有些API可能不会涉及到系统调用,比如数学库,字符串处理库,所以对这个“异常捕捉”有点疑惑,能给大家讲讲吗?
在这里又想到了一个问题,关于内存管理API的系统调用,受前面想法影响,觉得似乎假如这也是一个系统调用的封装,那么就不可能在编译阶段确定这个返回地址是多少了,而且这返回的只是一个地址,占用4个字节,那么编译程序不能在用户空间的数据段给它分配多余的空间。比如我申请100字节,如果这是个系统调用,大家想想结果会怎么样?后来去查有关资料有些地方说malloc不是系统调用,而是C库函数,这时才恍然大悟了,也就是在库中实现的内存管理API,该API只能管理用户进程虚拟空间,编译阶段申请的是用户进程空间的内存,实际没有物理内存对应,等程序被系统调度的时候发生缺页中断才去分配物理内存,因此这个也没有涉及到系统调用。据说有人在VC上验证过,在程序中不停的申请内存,函数返回正确,但是写入的时候发生错误提示内存不足。 学习。。。。。 同问 做个记号,OS 与 APP ,影子函数,系统应用分离。
这个问题也考虑过,在一些小OS中,系统与应用的分离的确很有用,有时间好好学习。 回复【56楼】liuweiele Liuwei
-----------------------------------------------------------------------
能根据你这个操作系统,简化简化再简化写个最简单的例子给大家学习学习吗?
只要能让大家明白OS 与 APP ,影子函数,系统应用分离。 回复【62楼】yisea123
回复【56楼】liuweiele liuwei
-----------------------------------------------------------------------
能根据你这个操作系统,简化简化再简化写个最简单的例子给大家学习学习吗?
只要能让大家明白os 与 app ,影子函数,系统应用分离。
-----------------------------------------------------------------------
等有空了之后. 回复【63楼】liuweiele Liuwei
回复【62楼】yisea123
回复【56楼】liuweiele liuwei
-----------------------------------------------------------------------
能根据你这个操作系统,简化简化再简化写个最简单的例子给大家学习学习吗?
只要能让大家明白os 与 app ,影子函数,系统应用分离。
-----------------------------------------------------------------------
等有空了之后.
-----------------------------------------------------------------------
期待。 感谢liuweiele 有些东西还是得自己琢磨,在没有弄清楚原理之前别人就有资本搞技术封锁,所以咱自己搞清楚原理才是王道,何况有有linux这么好的教材呢,我想既然楼主开了这个帖子,而且还有这么多人感兴趣,不如大家找个人组织一下搞搞看?以现成的OS改造也行。 赞同楼上的 应该称不上是"技术封锁"吧?
是你没有别人领悟得早。
人家研究出来的东西,没有义务都告诉你。所以不要太指望人家了,有兴趣的人一起研究就是了。 我也没说别人有义务把他的东西公布出来啊,我的意思是只有我们自己研究出原理了才有跟别人讨论的地位,否则别人凭什么给你,等我们弄得差不多了别人才有可能会一起来讨论,所以提议大家一起研究一下,如有人组织我参加一个。 大家组织组织 一起来研究吧 这方面liuweiele Liuwei是专家 mark liuweiele 还不出手啊 呵呵 回复【73楼】WXF_mabg
liuweiele 还不出手啊 呵呵
-----------------------------------------------------------------------
最近这几个月在忙一些比较急的新项目,国庆假期休息,昨天去 深圳会展中心 车展现场,物色了一下... hao 期待 这么好的帖子别沉,大家继续讨论啊! mark MARK mark 同问 论坛里面有个关于 LUA 的帖子,给这种 OS 与 APP 的分离指出了一个方向,最近正准备学习 LUA。 MARK! 今天也想到了os与应用程序分离的问题。因为今天见到了公司的兰德手持机m73,mcu芯片lpc2294,操作系统ucosII.开发方式是编译好的二进制程序下载进去,就能直接用。让我相信了ucos也可以这么的开发应用程序。 一般情况下,一个可执行二进制程序(更确切的说,在Linux操作系统下为一个进程单元,在UC/OSII中被称为任务)在存储(没有调入到内存运行)时拥有3个部分,分别是代码段(text)、数据段(data)和BSS段。这3个部分一起组成了该可执行程序的文件。
(1)代码段(text segment):存放CPU执行的机器指令。通常代码段是可共享的,这使得需要频繁被执行的程序只需要在内存中拥有一份拷贝即可。代码段也通常是只读的,这样可以防止其他程序意外地修改其指令。另外,代码段还规划了局部数据所申请的内存空间信息。
(2)数据段(data segment):或称全局初始化数据段/静态数据段(initialized data segment/data segment)。该段包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据。
(3)未初始化数据段:亦称BSS(Block Started by Symbol)。该段存入的是全局未初始化变量、静态未初始化变量。
而当程序被加载到内存单元时,则需要另外两个域:堆域和栈域。图1-1所示为可执行代码存储态和运行态的结构对照图。一个正在运行的C程序占用的内存区域分为代码段、初始化数据段、未初始化数据段(BSS)、堆、栈5个部分。
(4)栈段(stack):存放函数的参数值、局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。
(5)堆段(heap):用于动态内存分配,即使用malloc/free系列函数来管理的内存空间。
在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并将在内存中为这些段分配空间。栈段亦由操作系统分配和管理,而不需要程序员显示地管理;堆段由程序员自己管理,即显示地申请和释放空间。
另外,可执行程序在运行时具有相应的程序属性。在有操作系统支持时,这些属性页由操作系统管理和维护。
二、UC/OSII任务结构
严格说上面所说的可执行代码是一个单独的运行单元。在UC/OSII操作系统中,任务是事件运行和管理的基本单元;在Linux操作系统中,进程是事件运行和管理的基本单元。因此,在程序处理时,UC/OSII中的任务和Linux系统中的进程就相当于前述那段可执行代码,拥有基本的数据段、代码段、BSS段、堆空间和栈空间。
一个UC/OSII任务至少包含可执行代码、栈和任务控制块(TCB)。另外,可以选择性地使用相当于堆空间的动态内存空间。如图1-2所示,程序代码对应前述C语言可执行程序的代码段、数据段和BSS区,也就是可执行文件部分。栈对应前述的栈域,而任务控制块则是由操作系统管理的任务基本属性。在基于UC/OSII操作系统的应用程序中,任务的基本属性(由任务控制块提供)则由操作系统管理。在UC/OSII操作系统中,任务需要构建自己的栈空间,自己申请空间并设置自己的属性。
图1-2 C可执行程序与UC/OSII任务基本属性对照图
三、UC/OSII任务栈
1、栈空间数据类型
栈在实现上就是一段连续的内存空间,UC/OSII采用数组来实现。在使用OSTaskCreate()函数进行任务创建时,需要指定该任务栈空间的栈顶位置,即OSTaskCreate()函数的第3个参数。UC/OSII将任务栈空间的数据类型重新定义为OS_STK,实际上就是短整型,其定义如下。
//come from os_cpu.h
typedef unsigned short OS_STK;//栈空间每个单元的数据类型为16位短整型
在创建任务时,需要显示地声明一段全局空间作为该任务的栈。申请一段栈空间的示例代码如下:
#define TASK_STK_SIZE 128 //此大小可根据需要重新定义
OS_STK AppStartTaskStk; //全局变量
2、栈增长方向
在不同的处理器类型中,栈的增长方向不同,有些处理器规定栈的增长方向从低地址向高地址增长,而有些处理器则规定栈的增长方向从低地址向高地址增长。UC/OSII使用宏OS_STK_GROWTH来标识栈的增长方向。
//come from os_cpu.h
#defineOS_STK_GROWTH 1 //值为1标识栈增长方向从高到低,为0则低到高。
在申请全局数组空间时,是从低向高地址申请的。也就是说,数组的第1个成员a位于低地址,而最后一个成员a则位于高地址。如果一个系统栈增长方向是从高地址到低地址,则需要将a设置为栈顶;如果一个系统栈增长方向是从低地址到高地址,则需要将a置为栈顶。 再发一个介绍二进制可执行程序的文章片段:摘自《一步步写嵌入式操作系统--ARM编程的方法与实践》第8章运行用户程序
第8章运行用户程序
经过了不懈的努力,现在我们的操作系统已然有了一点大家风范。
然而,无论我们如何强调操作系统的重要性,如何想尽办法来提高操作系统的运行效率和稳定性,都不得不承认一个事实,那就是我们的操作系统目前似乎什么有用的事都做不了。它不能用来听歌、看电影,不能玩游戏、上网……
事实上,一个操作系统的作用不是发挥在功能上,而是发挥在机制上。就像Windows一样,仅仅装上了Windows操作系统,基本上是用不了的,你要额外装一个QQ工具才能聊天,要装一个客户端,才可以玩网络游戏,所以,操作系统并不能让你直接去做什么,而是在你想做什么的时候,尽最大可能地为你提供足够的支持,当然操作系统的作用还包括了资源的适当分配和系统监控,保证了玩游戏的时候不会因为程序抢占了所有的资源而导致你无法聊天。
与之对应的,应用程序负责的正是功能上的实现。在强大的操作系统支持下,应用程序能够更自由地实现各种复杂功能。这些程序虽然可以通过与操作系统相类似的方法编写和编译并被操作系统所调用,但是很多时候,它们不能享受与操作系统相同的待遇,并且要受到来自于操作系统的限制和管理。
从这个角度上看,操作系统就像一位司令官,自己并不会亲自上战场打仗,而是分配和调动手下的士兵,尽可能为他们提供充足的枪_支弹_药去赢得战场上的胜利。而这些冲锋陷阵的沙场战士们就是用户应用程序。
我们的操作系统现在已经成长为了一名合格的司令官。于是,本章的重点内容将会是如何让我们的操作系统有效地运行应用程序。
8.1二进制程序的运行方法(1)
其实,让我们的操作系统运行一个应用程序是非常简单的。不要忘了,应用程序也是程序,操作系统也是程序,让一个程序去调用另一个程序,又会难到哪儿去呢?
在ARM体系结构中,实现程序的跳转有很多种方法,这些内容在前面我们也都有介绍。
但有一点我们需要强调,我们这里的应用程序指的都是独立程序。因为有的嵌入式操作系统将应用程序定义成实现具体功能的程序,在这样的操作系统中,应用程序往往会与操作系统内核统一编程以实现程序的简化。
既然程序是独立的,那么一些简单的程序跳转方法就不能实现应用程序的运行了。此时我们可以获取应用程序的入口地址,使用mov指令来实现。
应用程序的入口地址应该怎样获取呢?目前没有什么好办法,只能让操作系统事先跟应用程序商量好,比如让一个应用程序在编译时就链接到某地址处,然后操作系统要把应用程序复制到链接时的内存位置,最后调用mov指令将PC寄存器的值赋值成该地址,实现程序运行。
下面我们通过一个小小的例子,看看最简单的应用程序调用过程是如何实现的。
代码8-1
int main(){ const char *p="this is a test application\n"; while(*p){ *(volatile unsigned int *)0xd0000020=*p++; };} 代码8-1就是这样一个应用程序。为了保证代码的简单,程序仅仅打印出"this is a test application\n"这个字符串,以证明应用程序的正常运行。
然后,我们需要将代码8-1保存到文件之中,命名为"main.c",把这个文件保存到代码根目录的"tools"文件夹里,然后切换到这个文件夹并使用如下命令编译这段程序。
命令8-1
arm-elf-gcc -e main -nostartfiles -nostdlib -Ttext 0x30100000 -o main main.c 这些编译选项的具体含义前面都已经介绍过了。虽然这里编译的是用户应用程序,但我们却不能像通常编写程序那样使用标准库函数和启动程序。广义上讲,标准库函数也属于应用程序的范畴,但此时这些函数库尚未建立。因此,目前的应用程序会显得非常简陋,我们既不能在程序中使用标准库中的函数和头文件,也不能在编译程序的过程中链接它们。
与此同时,为了保证程序能够正常运行,我们必须在编译时指定程序的运行地址,让它恰好落在有效的内存中。于是程序选定了0x30100000这个地址。回忆一下前面的内容,我们在MMU一节中将实际的物理地址映射到了虚拟地址中相应的位置。因此在虚拟地址空间中,0x30000000之后8M的内容都是有效的,留出操作系统自身所占的内存之后,0x30100000这个位置就非常合适了。
既然代码已经编译完成了,那么我们的应用程序是不是就可以运行了?
不,现在还不是时候。要知道,GCC默认只会生成ELF格式的文件,这是一种非常常用的可执行程序的文件格式,但因为这样一个可执行的文件包含了除代码和数据之外的附加信息,所以不能直接拿来运行。有关ELF文件格式的详细内容,我们稍后会有介绍。 如果是单任务的话,很容易做到OS和APP分离。
从另外一个角度来看,OS和APP的关系就象主程序和子函数的关系。
在51单片机里,Keil的MDK有个给子程序定位的功能,就是在主程序里声明子函数的烧写位置,这样编译器就能告诉主程序去那里
找子函数。该子函数只要做成{}形式的空函数即可,就是所谓的影子函数了,将子函数和主程序一起编译,就成了 OS 。
另外再单独建一个工程,只有上面说的子函数,同样指定烧写位置,在这里将该子函数实例化,然后编译,就成了 APP。
最后就是将OS和APP烧写到各自的位置去。
以后只要改动有子函数(APP)的实例的工程,然后单独烧写到指定的位置就可以了,主程序(OS)那里就不用再烧写了。 mark 学习了,好东西 看看我写的操作系统SmileOS! 本帖最后由 wyoujtg 于 2012-6-11 23:15 编辑
一点愚见,不一定正确。
我觉得有以下几种方法:
1. 如果整个系统只需要加载单个APP,那么可以在OS里做个函数指针表,保留一段内存用于加载APP,这个APP链接时就链接到这块内存的起始地址,OS拷贝APP到这块内存,启动APP时传入这个函数指针表的地址,APP就可以调函数指针表里的函数。(当然你也可以在链接OS时指定这个函数指针表的位置,这样启动APP就无须传入函数指针表的地址,因为你懂的)
2. 如果整个系统只需要加载单个APP,在OS里实现软件中断的支持,根据EABI,软件中断号通过R7传递参数,参数可以通过R0-R3传递,保留一段内存用于加载APP,这个APP链接时就链接到这块内存的起始地址,OS拷贝APP到这个空间,启动APP,APP可以通过软件中断调用OS提供的函数。
3. OS实现装载可重定位ELF文件的LOADER,OS里做个函数指针与函数名称的映射表,分配内存,OS装载APP到这块内存,需要符号时就在映射表里找,完成重定位后就可运行这个APP了。
4. 如果支持MMU,可以实现多进程了!具体方法和第二点类似,可以参看我写的多进程操作系统SmileOS mark 围观高手们激烈讨论 liuweiele 发表于 2011-9-26 12:25
回复【62楼】yisea123
回复【56楼】liuweiele liuwei
------------------------------------------------ ...
大神,有空教教我们OS与APP分离的设计方法。讲一下原理也行。 mark。标记以后再看 mark 围观, 有STM32 之类的应用例子吗? 、、、、、、、、、、、、、那全局变量和静态变量该如何处理呢 Mark!!! Mark!,以后学习 学习了~~~ 1.看os的map文件,可以得到所有函数的入口地址,或者指定函数入口地址
2.使用函数指针,强制跳转 回复很精彩,学习了。 如果是用 arm mdk,在 os 工程的 link 属性页的 "Misc control" 输入框加入 " --symdefs my_os.sym " 链接后得到符号信息(有点像 dll 的 lib 文件作用)
然后,在 app 工程的 link 属性页的 "Misc control" 输入框加入 " my_os.sym " 就能直接用 os 里面的函数、变量了。
SYM 文件的内容像下面这个样子:
#<SYMDEFS># ARM Linker, 5.99 : Last Updated: Mon Dec 15 18:42:01 2014
0x00000000 N __ARM_use_no_argv
0x0000000c D OSVectorTable
0x00000065 T rt_main
0x000000b1 T ISR_DUMMY_Handler
0x000000b3 T ISR_IART_Handler
0x000000bb T ISR_RST_Handler
0x000000df T ISR_TIM1_Handler
0x00000164 D ISR_PrefetchPadding
0x00000171 T __main
0x00000179 T __scatterload
MARK.围观学习。
页:
[1]