1、处理器

处理器:微控制器vs为处理器

为控制器:cpu+片内内存+片内外设

微处理器:cpu

注意:微控制器具有成本低,功耗低等优点,常用于嵌入式系统设计。

对于软件工程师而言,微控制器和微处理器没有任何区别。

寄存器分类:

cpu寄存器:专用指令执行,数据运算,变量处理,参数传递。

外设寄存器:用于控制外设的行为和工作方式,寄存器值的配置需要根据芯片手册完成。

处理器中的关键寄存器:

PC-程序计数器(指令指针IP):

没执行一条指令,PC中的值就会发生变化。

PC始终保存下一条cpu要执行的指令地址。

SP-栈指针(Stack Pointer)

始终指向栈空间的顶端,实现LIFO特性。

保存中断断点,保存函数调用返回点,保存cpu现场数据等。

PC和SP的使用案例-函数调用:

数据在栈上,当call函数调用时,pc指针指向了被调用函数的地址,执行完如何返回?cpu不会跳转,需要sp配合,pc跳转之前会将上下文信息保存到栈上,保存完sp指向了上下文信息的末端,这样pc就跳转执行被调用函数当return后pc就返回,pc找到sp指向的位置(栈顶),视频怎么回到原来的位置? 在借助另一个bp(基址)寄存器,在sp指向发生变化之前(保存上下文之前)会使用bp寄存器保存上一次sp的位置。保存之后,函数返回前,sp就知道要返回的位置,于是bp和sp之间的内容就是保存的上下文信息。这样就能恢复pc之前的运行参数了。pc就能跳转回去了。上下文信息包括bp寄存器的信息,bp之前指向了上边的位置,然后保存上下文信息的时候,之前的bp也被保存下来了,保存到之前bp和后来bp的位置,正因为保存了bp的值,才敢改变bp的值。

处理器的io操作:

处理器与外设之间的数据通信通过io操作完成。

内存映射io空间:

外设通过精密的硬件连接映射到处理器的地址空间。通过地址访问的方式与外设进行通信。


设备地址映射示例:

地址:0xFFFF (片选)1234 (地址线)

处理器启动过程简介:

处理器上电后,pc寄存器固化了一个默认值。

pc默认值用于决定第一条执行指令。

第一条执行指定隶属于启动程序。

第一条指令(地址0):

启动程序(Bootloader):

系统上电后运行的第一个程序bootloader(Not不是 OS)

根据运行阶段,体积和功能的不同分为三个部分:

BL0-固化于硬件中,用于初始化硬件,加载并运行BL1.

BL1-存储于外部设备中,用于初始化主存,加载并运行BL2.

BL2-存储于外部存储设备中,用于引导操作系统执行。比较大

示例分析-s3c6410启动过程:

上电,pc指向第一条要执行的指令在IROM中,初始化硬件(包括内存),将外部存储设备中的bl1加载到内存中执行,bl1之后初始化硬件设备SDRAM(主存),目的是将bl2加载到主存中,bl2执行,其他初始化,加载操作系统加载到主存中,将执行权交给操作系统,操作系统执行。外设系统盘:bl1,bl2,系统,文件。

bootloader与BIOS区别:

地位功能差不多。

2、处理器中

中断的概念和意义:

中断是一种处理器与外设进行通信的机制。

用于“通知”处理器外部有“重要事件”发生。

一般情况下,中断需要被处理器响应。

中断服务程序(ISR)

1、从外设中读取中断状态寄存器的值,以便了解中断类型。

2、根据中断类型具体设计处理逻辑。

3、清除外设状态寄存器中的中断标识位。

4、清除处理器中的中断标识位。

软件工程师眼中的中断服务程序:c函数

不能有返回值,不能有参数传递。

必须短小高效,避免浮点运算(耗时)。

_interrupt doubt compute_area(double radius)

{

double area=PI * radius*radius;

printf("\nArea=%f",area);

return area;

}

上边的函数不能作为中断,printf-->io耗时

中断返回到处理器被打断的地方.

中断的意义:

应用程序不必关心中断的发生与处理。

中断服务程序不必关心应用程序的执行状态。

中断是“上层应用”与“底层代码”的“分隔边界”

App Code<---正常执行--处理器(中断信号)--中断发生-->ISR

中断的类型:

硬中断:通过处理器中断信号线产生的中断。

软中断:通过非法指令或特殊指令触发的中断。(陷阱或异常)

中断的优先级:

多个中断同时出现时,处理器先响应高优先级的中断。

低优先级中断的ISR执行时,可以被高优先级中断再次打断。

ISR比app code拥有更高的执行优先级。

中断的应用-程序断点:

断点指的是调试工具用于暂停代码执行的指令位置。

断点的实现原理为处理器的中断支持。

软件断点:利用非法指令异常产生中断实现。

硬件中断:利用中断寄存器的特性产生中断实现。

程序断点的实现原理:

1、获取原程序指定行对应的代码地址。

2、把代码地址中的指令替换为中断触发指令。

3、在中断服务程序中将控制权交给调试程序。

4、调试程序读写原程序上下文信息。

5、调试程序将代码地址中的指令还原。

6、原程序从断点处继续向下执行。

实现原理:图

jmp(源码)-->指令替换-->Int 3 (特殊的中断触发指令)

->中断触发ISR->控制权转移(中断触发指令最终会被os捕获,并翻译成对应消息(signal)发送给调试程序)

->调试程序->(查看上下文信息,修改变量值等)->指令还原->jmp(pc设置地址)

一个工程产品案例的剖析:

背景:嵌入式实时系统对时序的要求比较严格。

各个线程的执行有相对严格的时间要求。

痛点:断点调试在嵌入式实时系统中不适用。断点不符合时序要求。

常规解决方案-日志调试法

在代码中的关键位置添加打印语句。

打印语句尽可能详细的打印上下文信息(函数名,局部变量等)

当系统出现问题时,查看日志文件分析问题。

存在问题:

不易维护:打印语句分散于产品代码的各个角落。

影响效率:过多的打印意味着过得io操作,影响产品整体执行效率。

分析困难:当日志多的时候,很难定位问题。

也许只有添加打印语句的工程师看得懂日志输出。

一个疯狂的想法:

同时结合日志调试法和断点调试法的优先,使得实时系统调试时,能够任意查看指定代码行上下文的信息,并且,不增加打印语句,不暂停执行。

解决方案:

1、获取原程序指定行对应的代码地址。

2、把代码地址中的指令替换为中断触发指令。

3、在中断服务程序中抓取全局信息和栈信息。

4、抓取的信息发送回调试程序解析并输出。

实践结果-MProbe

基于ARM+Linux平台实现

通过中断原理成功获取上下文信息。(256字节)

完全不影响程序的执行时序。

产品关键技术点:中断,ISR,编译信息,GDB(开源调试器)

GUI(界面),Socket(网络编程),多线程

1、line=>adress(指令地址)->分析编译器gcc中间输出 图2.3

Q/A :GDB重要工具

替换指令:linux指令地址对应的内存空间是只读的,用户态发生问题,可以在内核态通过系统调用将代码段改为可写的,这样就能替换了。

二进制代码:编译过后的代码。编译器管不着,非法代码也能运行。

3、处理器下

内存管理单元(mmu)

现代处理器中对内存进行高效管理的功能单元。

操作系统利用内存管理单元能够实现:

虚拟内存:

内存保护:

有意思的问题:

下面程序,两次运行的输出是否相同?为什么?

int g_v =1;

int main(int argc,char* argv[])

{

printf("g_v=%d\n",g_v);  ==>进程1 &g_v=?

printf("g_v=%d\n",&g_v);  ==> 进程2  &g_v=?

system("pause");

return 0;

}

理论上不同,实际相同的,虚拟内存?

有意思的问题:下边表格

                         OS

可执行程序:  进程1

                      进程2

存储器         物理内存

理论上,不同进程在内存中的不同位置执行,因此,全局变量的地址不同。

进程被遗忘的事实:

应用程序开发时,面对的内存为虚拟内存。

虚拟内存模式下使用的地址为虚拟地址。

每一个进程拥有独立私有的虚拟地址空间。

虚拟内存与实际物理内存无关,是一个假想的足够大的内存。

思考:内存需求量为1G的应用程序释是否能够运行与硬件内存为256的计算机?

虚拟内存的意义:虚拟内存能够支持多个大内存需求量的进程同时运行与较小的物理内存中。

M(P1)+M(P2)>Physical

OS(p1,p2)(进程1,进程2)------full code forp1 full code for  p1 交换区

交换区(外部磁盘):暂时存放进程的多数代码。

操作系统和处理器共同完成。

虚拟内存的机制:

虚拟内存需要重新映射到物理内存。

虚拟地址映射到物理内存中的实地址。

每次只有进程的少量代码在物理内存中运行。

大部分进程代码位于存储器中。

页式内存管理:

页式内存单位,指一定数量的内存(如:4k)

虚拟内存和物理内存以页为单位管理。

进程的活动页被载入内存时,记录页地址的映射关系。

虚拟地址-》映射表---》物理地址

页式管理法将内存分为两个部分:(p,d)

p-地址高位,页面号。

d-地址地位,页内偏移量。

图:

每个线程对应的都是虚拟内存:内存非常大4g。

查表如果没查到?虚拟页号不存在,虚拟地址对应的页并没有加载到内存中,这时

图:

思考:

一下几种方式为什么能够提高电脑性能?

更换主频更高的处理器(同系列):运算能力更强

增加物理内存大小:内存与磁盘换入换出次数少了

更换ssd固态硬盘:固态上有交互区,换入换出次数没变,读写磁盘时间短了,换入换出时间快。页面换入换出快。

Q/A:

内存保护:内存管理单元在映射时属性中能判断是否合法。

页表也占内存。

页映射表:

活动页太大:映射表小;

活动页小:映射表大。

4、处理器续

一个工艺上的问题:

处理器和内存所使用的半导体器件工艺不同。

工艺的差异导致了处理器与内存的速度差异。

数据处理时,处理器总是需要等待内存。

V(处理器)>>>>>V(内存)

程序访问的局部性:

在短时间内,处理器访问的存储空间是一个很小的范围。

时间局部性:某个存储单元在短时间内很可能被再次访问。

空间局部性:某个存储单元的邻近单元在短时间内也被访问。

高速缓冲存储器的引入(cache):

cache是一种小容量高速存储器。

cache的存取速度与处理器的运算速度几乎同量级。

cache在现代计算机系统中直接内置于处理器芯片中。

cache解决方案:

在处理器和内存之间设置cache。

把内存中被频繁访问的指令和数据复制到cache中。

大多数情况下,处理器能直接从cache中取得指令和数据。

处理器的数据访问:图4.1

问题:内存和cache之间如何映射?

直接映射法:将cache和内存分成固定大小的快(如:512 Byte/块)

内存中的每一块在cache中有固定的映射地址。

映射公式为:

Pos(cache)=内存块号 % cache总块数

特点:任意一个内存地址都映射到cache中的一个固定位置。

地址划分:

标记(t位)cache块号(c位) 块内地址(b位)

映射原理:

根据访问地址的中间c位找到cache中的对应块。

比较地址的高t位是否和flag相同:

相同:直接读取数据。

不同:从内存中复制块内容。

当前处理器需要访问内存地址0x0240CH

1、地址划分:0000001  0010  000001100

2、根据0010直接访问cache中的第0010块。

3、匹配到0010块的flag是否等于0000001.相等:访问0010块中1100处的数据。不相等:从内存中读取块数据,更新cache

直接映射法特点:

优点:映射过程简单,所需耗时短。

缺点:当短时间内访问的地址有同余冲突时,会造成缓冲失效。

cache原理的软件应用:

项目背景:日志调试工具(Log Dog)

解决的问题:

日志对系统效率影响巨大,且不容易分析查看。

现有的日志系统无法高效的打印二进制数据。

自定义日志内容的解析方式。

对日志进行分类,并控制日志是否输出。

出现过的性能问题:

当短时间内有大量日志需要打印时,性能无法满足调试需要。

解决方案:

根据cache原理,设置二进制缓冲机制,尽量避开查找!

图4.3

message==》type==》int

每一个工程师都可以自定义日志类型。

自定义解析方式==》Lua Script

message==>Lua script(parser)  映射到解析器

script ==》id==》int

void process(Message* msg)

{

static parser* cache1=NULL;

static parser* cache2=NULL; // 自己练习

if((cache!=NULL)&&(cache->id==message->typee))

{ cache->handle(msg); }  //缓冲解析器直接用

else

{ parser* p=find_parser(msg->type);  //性能瓶颈 次数多

if(p!=NULL)

{  p->handle(msg); 

   cache=p; }  }

}

与哈希解决方法差不多?

5、C语言编译器

GCC与gcc有什么不同?

GCC(GNU Cmopiler Collection):

GNU编辑器集合,包含众多语言的编译器:c,c++,java,D,objective-c,etc,

gcc:特指GCC中的C语言编译器。

GCC vs 嵌入式

多数嵌入式操作系统都基于GCC进行源码编译:linux,vxworks,Android,etc

实际开发中的使用:

内核使用:gcc

应用开发;gcc/g++/gdc

什么是交叉编译?

背景:嵌入式设备往往资源受限,

不可能在嵌入式上直接对处理器进行编程。

解决方案:

在开发主机(pc)上对源码进行编译。

最终生成目标主机(嵌入式设备)的可执行程序。

gcc是如何进行交叉编译的?

配置目标主机的编译工具链(如:arm-linux)

配置工具链的具体版本:

根据具体的目标代码选择相应的工具链版本。

正确使用关于硬件体系结构的特殊编译选项。

案例:大型企业嵌入式开发环境

服务器集群,用于代码存储,版本控制,交叉编译,文件追踪等。

代码编写环境:windows、linux

授权pc,用于实际开产开发,调试,测试,等。。

初识编译器:

编译器:预处理器,编译器,汇编器,链接器

你不知道的事:

file.c,file.h--->预处理器(处理宏)-->file.i(C语言)-->编译器-->file.s(汇编语言)-->汇编器-->file.o(目标代码)

扩展问题:

如何理解“多语言混合开发”?

多语言混合开发方式一:

x语言 y语言 z语言==》目标平台汇编语言=》目标平台汇编器=》可执行程序

行业案例:.net framework

c#  c++ vb==》MSIL(微软中间语言)=>application

多语言混合开发方式二:

x语言->x.dll(win上的动态链接库)   y语言->y.a  z语言->z.dll

目标平台链接器将上边的库链接起来--》可执行程序

方式一是都到汇编语言(语言到语言,语言层面的兼容),方式二是都到二进制文件在链接(二进制代码层面的兼容)

行业案例:qq

qq.exe--》AppFramework.dll-zlib.dll-lua.dll-sqlite.dll

多语言开发方式三:

x语言-》x.exe y语言-》y.exe z语言-》z.exe

===>进程间通信协议==>可执行程序

行业案例:Eclipse

eclipse.exe-加载->(jre.exe) App Entry(Jar Package)-依赖->swt.jar jface.jar core.jar ->swt-xxx-win32.dll

jre  java运行时,jre本质是java虚拟机

gcc关键编译选项一:

预处理指令:gcc -E file.c -o file.i(-E 表示预处理器工作,文本替换)

编译指令: gcc -S file.i -o file.s  (-S c代码到汇编代码)语言到语言

汇编指令:gcc -c file.s -o file.o  (-c 将file.s生成二进制代码)

gcc关键编译选项二

生成映射文件:gcc -Wl,-Map=test.map file.c 

 用来找全局变量或函数的地址,放在哪个位置,全局变量和函数的地址编译结束就知道了地址,不需要运行时决定,不需要动态决定,都是虚存地址

全局变量g_global:bss段

全局变量g_test:   .data段

函数main func: .text段

宏定义:gcc -D'TEST="test"' file.c   通过命令行定义宏TEST

获取系统头文件路径: gcc -v file.c

gcc关键编译选项三:

生成依赖关系:获取目标的完整依赖关系 gcc-M test.c

获取目标的部分依赖关系: gcc -MM test.c

gcc关键编译选项四:

指定库文件及库文件搜索路径:

-L选项:指定库文件的搜索路径。

-l选项:指定库文件。

gcc test.c -L. -Ifunc

gcc -c func.c -o func.o //编译 .c到.o
ar crs libfunc.a func.o  //将.o打包成库文件libfunc.a
gcc test.c -L. -lfunc   // 使用库文件 -L当前目录搜索 -l库文件名字写出就行前缀后缀可不写,生成可执行程序

./a.out     //

6、辅助工具

什么是开发环境?

构建环境:代码编写,程序编译,版本控制(可选)

调试环境:用于定位问题的辅助工具集。

测试环境:用于验证目标程序是否满足用户的显性需求和隐形需求。

嵌入式开发中的时间分配:

代码编写及目标构建(20%); 测试,调试,bug修复(80%)

如何提高开发效率?

GNU为GCC编译器提供了配套的辅助工具集(Binutils)

工具名          功能简介

addr2line      将代码地址转换为对应的程序行号

strip        剔除可执行程序中的调试信息

ar            将目标文件打包称为静态库

nm        列出目标文件中的符号及对应地址

objdump    查看程序段信息及反汇编

size        查看目标文件中的段大小

strings  查看目标文件中的字符串

addr2line  :

   将指定地址转换为对应的文件名和行号,常用于分析和定位内存访问错误问题。

void func()

{ /* oops!! */

*g_pointer= (int)"d.t.soft";

return ; }

add2line示例:定位0地位访问

1、开启core dump选项。 用来记录程序奔溃最后一刻内存,寄存器的状态

ulimit-c unlimited

2、运行程序,并生成奔溃时的core文件。

执行导致程序奔溃的测试用例。

3、读取core文件获取IP寄存器的值(0x08048000)

dmesg core

4、使用addr2line定位代码行 程序必须是调试版本的

addr2line 0x08048000 -f -e test.out

strip:剔除程序文件中的调试信息,减少目标程序的大小。一般在程序发布前都需要将调试信息剔除。过多的调试信息可能影响程序的执行效率。

strip test.out 

ls -l test.out //详细信息包括大小

注意事项:

几乎所有的调试辅助工具都依赖于目标文件中的调试信息。

调试信息的运用能够快速定位问题。

使用gcc编译程序时使用-g选项生成调试信息。

发布程序时在考虑是否使用strip剔除调试信息。

ar   打包目标文件 :ar crs(所有的.o) libname.a(静态链接库) x.o y.o

解压目标文件:将静态链接库解压为.o文件

ar x(编译选项) libname.a

ls *.o  查看所有.o 文件

ar x libtest.a

ls *.o

func.o  test.o

nm: 列出目标文件中的标识符(变量名和函数名)

输出结果由三部分组成:{ 地址,段,标识符 }

示例:nm func.o

08048430(标识符对应的地址) T(标识符位于代码段) func(标识符的名字)

标识符说明:

段标识   说明

A           地址值在链接过程中不会发生改变

B或b     标识符位于未初始化数据段(.bss)

C           未定义存储段的标识符,链接时决定段位置

D或d      标识符位于数据段(.data)

N            调试专用标识符

R或r        标识符位于只读存储区(.rdata)

T或t        标识符位于代码段(.text)

U           未定义的标识符

nm func.o
00000000 T func //编译出目标文件还不能确定最终地址在哪里,链接时才知道,全零表示偏移

00000004 C g_pointer //不知道在哪里,4表示占四个字节的内存

 nm test.o
         U func  //使用func 但是不知道是什么链接时才知道
00000000 B g_global //=0 未定义,在bss段,在bss中偏移为0
         U g_pointer //用到不知道
00000000 D g_test //数据段 偏移0
00000000 T main  //代码段 偏移0 就一个函数

         U printf   // 库中定义的

objdump:反汇编目标文件,查看汇编到源码的映射

objdump -d func.o  //汇编

objdump -S func.o  //有映射,有原程序

查看目标文件中的详细段信息:objdump -h test.out

objdump -h 的输出说明

               说明

Idx          段下标

Name   段标识符(名字)

Size    段所占空间的大小

VMA    段起始位置的虚存地址

LMA    段在存储空间中的加载地址

File off 代码段在目标文件中的相对位置

Algn    段的边界对齐字节数

可执行程序的加载过程:虚存地址(VMA) 加载地址(LMA) 运行地址 3种场合

1执行可执行文件时,首先创建一个进程,分配虚存,然后将可执行文件中的段信息,此时用到file off,拷贝到虚存的虚地址处,就是编译时确定的段的起始虚地址VMA(起始虚地址保存在test.out中),这样虚地址就是加载的目标地址,加载的目标地址就是加载地址LMA,段加载到虚存中的目标位置处。所以LMA 和VMA相等,加载地址指的是段加载的终点也就是编译确定的虚存地址。最后执行应用程序。运行地址指的是实际运行的地址,实地址。加载地址指的是终点。

虚存是在内存中,是将可执行文件中代码段拷贝到虚存。

执行程序如何执行? 当所有段位于虚存中时,当pc指针指向代码段的第一条指令。执行代码段中的指令。就是下课中的_start的起始地址。

2嵌入式环境中执行可执行文件:

源代码-交叉编译->test.bin(目标文件)--烧写->Device(存储系统Flash 两种情况)

Nand Flash-->存储程序数据不能执行--加载到->RAM执行 上电时如何确定加载什么代码段,这就要知道代码地址了

这个地址叫加载地址:从加载地址处加载代码段。这样VMA!=LMA(加载的位置) 运行地址为程序实际执行的RAM中的地址,也就是实地址。

最后一种情况:嵌入式

烧写到设备的Nor Flash 。Nor Flash中的程序可以直接执行。到Nor里边执行相应的指令

LMA加载地址指的是Nor Flash中的地址,运行地址也是Nor Flash中的地址。加载地址=运行地址。

没有虚存地址VMA。

Size:获取目标文件中的所有段大小。size test.out 看看资源会不会限制

strings:获取目标文件中的所有字符串常量。strings test.out  分析字符串常量占的资源

猜你喜欢

转载自blog.csdn.net/ws857707645/article/details/80851881