#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事 原创 精华

唐佐林
发布于 2020-12-10 09:53
浏览
13收藏

接触鸿蒙设备开发有一段时间了,也是时候好好挖一挖鸿蒙设备程序的启动流程了。

 

破冰问题:鸿蒙设备程序从哪里开始运行的?

 

相信大家都已经非常清楚了,鸿蒙设备程序需要指定入口函数,具体表现在代码层面就是通过语句 SYS_RUN(app_entry); 指定,其中 app_entry 是设备程序入口函数名;而整个鸿蒙设备的启动流程也可以顺理成章的挖掘出来。如下图:

#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区

这看起来非常完美了,解决了所有问题!可是,我觉得还是有不清楚的地方,即:MODULE_INIT(run) 干了什么事?为什么最终会调用到 app_entry() 这个入口函数?

 

接下来,我们逐个问题的解决!

 

本质问题:MODULE_INIT(run) 干了什么事?

 

要弄清楚这个问题,就得先来讲讲 SYS_RUN() 究竟是什么?!有同学可能会认为 SYS_RUN(app_entry); 是一个函数调用语句,将设备程序入口地址注册到系统中,进而调用。从原理上这么理解没错,可细节上根本不是那么回事! SYS_RUN() 在用法上很像函数,但本质是一个宏!必须强调: 在 C 语言中无法在函数之外进行函数调用,而 SYS_RUN(app_entry); 出现的位置并不在任何函数中,所以它不可能是函数调用。那会是什么呢?真相只有一个,只可能是一个定义(声明)语句。为了证明这个结论,我们将 SYS_RUN() 这个宏彻底扒光了看个透彻。如下:

#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区剖析:

#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区最终,我们可以知道:SYS_RUN(app_entry); 是定义了一个名为 __zinitcall_run_app_entry 的函数指针,其类型是 InitCall,无论是否使用都不会编译报错,并且强制编译使其最终存放在名为 .zinitcall.run2.init 的段中。

 

好!接下来就可以直接分析 MODULE_INIT(run) 了。

#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区

MODULE_INIT(run) 展开之后根本看不出和 app_entry 有任何关系啊!我们用了九牛二虎之力把宏掰开了,可结果貌似一无所获!!!MODULE_INIT() 和 SYS_RUN() 之间的关系还是非常不明朗,接下来该怎么办呢?

 

仔细观察这两个宏拼接出来的符号!

#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区

可以发现它们都和 zinitcall 有关,并且我们也知道了 SYS_RUN(app_entry) 定义的全局指针就放在名为 .zinitcall.run2.init 的段中,所以可以推测:这个两个宏的关系是通过链接脚本关联的。

 

接下来,通过工具查看目标文件的段信息和符号信息。

#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区

通过输出可以知道,在名为 .zinitcall.run2.init 的段中确实存在 __zinitcall_run_app_entry 这个符号。

 

之后,动手翻源码。。。。

经过努力,我们可以找到 \code-1.0\vendor\hisi\hi3861\hi3861\build\build_tmp\scripts\link.lds 文件,并且发现如下的脚本代码:

#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区

 

这样就真相大白了:

SYS_RUN(app_entry) 定义的函数指针 __zinitcall_run_app_entry 通过强制编译的方式进入 .zinitcall.run2.init 段中。在链接脚本中定义的两个符号 __zinitcall_run_start (理解为数组名)和 __zinitcall_run_end 分别指向 __zinitcall_run_app_entry 所在数据段的起始位置和结束位置。 又因为 MODULE_INIT(run) 的功能就是遍历 __zinitcall_run_start 和 __zinitcall_run_end 所指定的区域(理解为函数指针数组),并调用每个单元(指针)所指向的函数,因此,__zinitcall_run_app_entry 所指向的函数必然被调用,即:app_entry() 必然被调用。

 

更进一步阅读这个链接脚本可知:目标文件中的 .zinitcall.run2.init  段最终会被链接并汇编进一个名为 .zInit 的数据段中!

 

查看最终可执行程序中的符号信息和段信息可证明这个结论。

#2020征文-开发板#SYS_RUN()和MODULE_INIT()之间的那些事-鸿蒙开发者社区最终可执行程序中存在 __zinitcall_run_app_entry , __zinitcall_run_start 以及 __zinitcall_run_end , 并且 __zinitcall_run_app_entry __zinitcall_run_start 的地址均为 0x004aeb1c,根据输出的段信息可知,它们均位于 .zInit 段中。证毕! 

 

总结:

  1. 通过强制编译链接构成一个全局指针数组(每个 SYS_RUN() 定义一个数组元素)
  2. 在链接脚本中定义符号自动确认这个数组的起始地址和结束地址
  3. MODULE_INIT() 通过遍历的方式调用数组元素所指向的函数

 

PS:

大家如果感兴趣可以自己亲手动手实验一下,所需材料和工具可在文末附件中下载。

1)编译附件中的 hello_world 工程(基于Hi3861)

2)将下面编译得到的目标文件拷贝到工具目录

        \code-1.0\out\wifiiot\obj\applications\sample\wifi-iot\app\hello_world\hello_world.o

        \code-1.0\out\wifiiot\Hi3861_wifiiot_app.out 

3)执行命令观察结果

        ./nm hello_world.o 

        ./objdump -h hello_world.o

        ./nm Hi3861_wifiiot_app.out

        ./objdump -h Hi3861_wifiiot_app.out

 

感叹一下,这真是一个精妙绝伦的设计方案!这样设计,理论上支持任意多的设备程序,开发者只需要简单的指定程序入口即可,完全不用关心背后的机制,也不用担心最多支持多少程序的问题。

 

这一招,学到了!!!

 

文中涉及多个进阶知识点,可参考狄泰软件学院《C语言进阶剖析教程》和《唐老师的私房课》。

 

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
hello_world.zip 983B 98次下载
tools.zip 844.31K 90次下载
已于2020-12-16 16:29:13修改
14
收藏 13
回复
举报
11条回复
按时间正序
/
按时间倒序
longlong899
longlong899

好文章,先mark,再慢慢品读!!

回复
2020-12-10 10:01:02
红叶亦知秋
红叶亦知秋

跟着老师走了一遍流程,学到了!

回复
2020-12-10 10:09:31
老克
老克

可以。  学到了啊!

回复
2020-12-10 10:44:22
卫斯理
卫斯理

唐老师又来社区炸街啦

回复
2020-12-10 10:57:57
唐佐林
唐佐林 回复了 卫斯理
唐老师又来社区炸街啦

分享心得哈,和大家一起学习鸿蒙。:)

1
回复
2020-12-10 11:39:39
艾那的小强
艾那的小强

这一招在rt-thread里学过。^_^

回复
2020-12-15 14:19:15
叶藟香
叶藟香

高手 高手 高高手

回复
2020-12-15 20:31:57
济南吴元超
济南吴元超

就喜欢这种知悉所以然的风格,108个大赞

回复
2021-1-29 15:16:23
谢基榕
谢基榕

SYS_RUN(app1)

要求  void app1(void) 能够有限时间结束吧?因为在那个 for 语句中是顺序执行的,

如果要实现一个事务性的服务过程呢?自己在这里创建线程吗?

回复
2021-6-27 17:18:52
鸿蒙开发
鸿蒙开发

谢谢老师分享

回复
2021-9-10 08:03:01
wx5952175d7f516
wx5952175d7f516 回复了 唐佐林
分享心得哈,和大家一起学习鸿蒙。:)

原来唐老师这几年一直在搞鸿蒙,这文章不错~

1
回复
2023-7-22 23:33:32
回复
    相关推荐