现在的位置: 首页 > 综合 > 正文

工作中与处理器有关的问题总结

2013年10月31日 ⁄ 综合 ⁄ 共 4130字 ⁄ 字号 评论关闭

经常遇到的典型问题或者说我能想到的有以下这些:

Ø

字节序

Ø

字节对齐

Ø

异常

Ø

符号位问题

Ø

堆栈溢出

Ø

空指针


Ø

编译
&
反汇编

字节序问题:
大端法小端法








其实说白了就是一个顺序问题。现代的计算机系统
一般采用
字节

(Octet, 8 bit Byte)
作为逻辑寻址单位。
当物理单位的长度大于
1
个字节时,就要区分
字节顺序



常见的字节顺序有两种:
Big Endian


Little

Endian.

Intel X86
平台采用
Little Endian
,而
PowerPC

ARM

MIPS
处理器则采用了
Big Endian



          
于是问题来了,一个两字节的数
0XABCD
,要存
储在
0-1
这两个字节中,那么
0
中是存
AB
还是
CD

PPC

ARM

MIPS
等选择了
AB

X86
选择了
CD.

          
说到字节序,一定要区分主机序和网络序。



网络序:
TCP/IP
各层协议将字节序定义为
Big-
Endian
,因此
TCP/IP
协议中使用的字节序通常称之为
网络字节序。




主机序:它遵循
Little-Endian
规则。所以当两台主
机之间要通过
TCP/IP
协议进行通信的时候就需要调用
相应的函数进行主机序(
Little-Endian
)和网络序(
Big-Endian
)的转换。


编程规范里面提到过数据结构设计需要考虑字节对齐。


          
所谓字节对齐,就是要求某个数据在内存中的起
始位置必须是该数据类型的
对齐大小
的整数倍。基本
数据类型(
char

int

long
等)的对齐大小等同于类
型的大小,结构体的对齐大小等同于其各成员变量的
对齐大小的最大值,数组的对齐大小等于其基本类型
的对齐大小。数据的大小必须是对齐大小的整数倍。

为什么要字节对齐?

           
因为某些处理器不允许
16
位和
32
位的数据在内存
中任意排放。




通常
32
位的处理器通过总线访问(包括读和写)
内存数据。每个总线访问周期可以访问
32
位内存数据
。内存数据是以
8
位的字节为单位存放的。假如一个
32
位的数据没有在
4
字节整除的内存地址处存放,那么处
理器就需要
2
个总线周期对其进行访问。通过合理的内
存对齐可以提高访问效率。

为什么要字节对齐?(续一)

          
访存是通过特定的指令完成的,为了减少访存的
时间,大部分
CPU
都有一次可以读写
N
个字节(
N
取决
于位宽)的指令,但是由于硬件上的限制,这些指令
都是有限制的,比如地址必须是
N
的整数倍(原因没
查到)。


          
在不对齐的情况下,有些处理器会通过组合指令
的方式达到目标。比如需要从地址
1
处取一个
unsigned
int
类型数据,可以先从
0
处取
4
字节,然后从
4
处取
4

节,再进行移位相或获得需要的数。如果是一个
int

型,则更加麻烦,因为移位的时候需要处理符号位,
对效率会有很大影响。

为什么要字节对齐?(续二)

          
mips
是定长指令的设计,每条指令都是
32
比特(
或许会有
64
的出现?)。

           32
位的指令长度也就意味着要传递一个
32
位的地
址起码需要两条指令(指令有若干位是操作码)。因
此如果在
mips
中如果需要实现不对齐的内存访问,需
要耗费更多的时间。于是
mips
干脆直接不处理这种情
况,遇到不对齐的直接
error





无论如何,为了提高程序的性能,数据结构(尤其
是栈)应该尽可能地在自然边界上对齐。原因在于,
为了访问未对齐的内存,处理器需要作两次内存访问

然而,对齐的内存访问仅需要一次访问。



一个字或双字操作数跨越了
4
字节边界,或者一个
四字操作数跨越了
8
字节边界,被认为是未对齐的,从
而需要两次总线周期来访问内存。一个字起始地址是
奇数但却没有跨越字边界被认为是对齐的,能够在一
个总线周期中被访问。




编译器对内存对齐的处理:
c
编译器默认将结构、
栈中的成员数据进行内存对齐。编译器将未对齐的成
员向后移,将每一个都成员对齐到自然边界上,从而
也导致了整个结构的尺寸变大。



字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:

1)
结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

2)
结构体每个成员相对于结构体首地址的偏移量(
offset
)都是成员大小的整数倍,

如有需要编译器会在成员之间加上填充字节(
internal adding
);

3)
结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最

末一个成员之后加上填充字节(
trailing padding


          
不同的
CPU
对异常的定义是不一样的,比如这段代码:
int
*piTest = (int*)3; *piTest = 1;
这段代码
PPC
能正常处理,
MIPS
则会抛出异常。另外还有一些修改
data
段的操作,在
PPC
上是
没问题的,
X86
上会异常。

          
异常一般通过中断处理,当
CPU
断定产生了异常的时候,
会产生一个中断,然后跳转到相应的中断向量去执行中断处理
。。


          
不同的
CPU
对异常的定义不一样,编码怎么办?按照最严
格的那个来。比如字节对齐,看起来较真了一点,但是这个在
那些容忍不对齐的设备上也是有好处的,可以提高效率。而修

data
段,本身就是一个不符合常理的操作。




符号位的问题可以肯定的是这个和
mips
的一个很重要
的特性有关:
mips
只能处理
32
位的数据。也即如果对
两个
char
相加,
mips
需要把两个
char
填充成两个
32

的数,并对符号位进行处理,以保存溢出等特性,然
后再相加。


           
这个也要求大家在定义数据的时候,需要考虑好
,这个数据的特性,是有符号的还是无符号的




从物理上讲,堆栈就是一段连续分配的内存空间。在
一个程序中,会声明各种变量。静态全局变量是位于
数据段并且在程序开始运行的时候被加载。而程序的
动态的局部变量则分配在堆栈里面。



堆栈溢出就是不顾堆栈中分配的局部数据块大小,
向该数据块写入了过多的数据,导致数据越界。结果
覆盖了老的堆栈数据。




有两种情况会引起堆栈溢出,第一种可能出现的情
况是如果你定义了大数组会导致堆栈溢出;第二种因
为函数的参数和里面声明的局部变量,


都是在栈内分配空间。所以如果递归调用层次过
深的话,就有可能栈溢出。不要在递归函数内
申请大的空间。

          
这个章节一般不会用到,但是如果能理解对提高问题处理能力还是有帮
助的。这个是编译模拟器的
.a
时的截图:

          



          
红色部分大家应该能注意到这个名称包含了一个

pentium

,实际就是
奔腾处理器的名称。从这个可以看出,编译器是直接和处理器相关的,不同
的处理器会有不同的编译器。不知道谁看过《疯狂的程序员》,里面就提到
一个汇编的问题,说懂得汇编的人有个好处,不用等那些最新的
CPU
的编译
器,因为厂商推出一款新的架构的处理器的时候,有可能会先推出汇编编译
程序,其他语言的编译器可能需要过一阵子才能发布。之所以这样是因为不
同的
CPU
的指令集是不一样的,而汇编基本是和指令集挂钩的,因为汇编解
析程序实现比较容易,而像
C
这种高级语言的解析则费劲的多。


          
编译中大部分的错误提示都是语言性质的提示,处理器性质的提示很少

          
编译过程我们不用太多关注处理器的信息,那是编译器的
任务。但是反汇编的时候,如果需要理解反汇编出来的代码,
则必须要理解相关处理器的指令集。我认为应该多少学习一下
汇编代码。


          
而我们为什么要会反汇编。我在
2126EA-MA(B)
这款
MIPS
芯片的设备中深刻体会了一把,也吃到了不少甜头。个人觉得
反汇编有两个作用:
1

debug
。有些看代码很难找的
bug
在反
汇编下很容易

原形毕露


2
、更好的理解你的代码。反汇编出
来的东西更接近指令,对比阅读能让你更好的理解同一个功能
,用不同的代码写会有什么不同的指令体现,什么方式效率更
高。这也是我上面提到的大家最好抽空学学汇编语言,有助于
理解代码,更好更快的定位问题。

以下是个人的一些对处理器、编译器的理解后总结的编
程习惯:



(一)
不要有太多的条件分支。这个与处理器的流水
线技术有关。一条指令的执行一般会分成几个步骤,
比如取指、译指、执行指令等,很多处理器有
7-20

左右的步骤,每条指令都需要顺序经过每个步骤。另
外指令是需要依次执行的。以三个步骤为例说明流水
线技术。每个步骤都需要特定的硬件模块执行,比如
取指需要取指模块完成,译指需要译指模块完成。假
设每个步骤耗时为
t
,如果处理器只有执行结束一条指
令的所有步骤之后才执行下一条指令,那么三条指令

A

B

C
)总共需要
9t
时间。如果采用流水线技术
,那么当
A
进入译指模块的时候,取指模块已经取出
B

的指令了,当
A
进入执行阶段的时候,
B
进入译指阶段,
C
进入取指阶段。这样下来三条指令只需花费
5t
的时间,当指
令很多的时候,每条指令花费的时间应该是
t
。当然这个是理想情况,导致流水线中断的有多个原因,比如数据依赖
和条件分支,这里说一下条件分支。在条件分支下,
A
的下一条指令可能不是
B
而是
E
。如果
A
执行完成之后,发现
下一条指令是
E
而不是
B
,这个时候,
B
已经完成了译指,
C
完成了取指。但是不但白做了,还增加了额外的负担:
清空流水线,这个对效率的影响是很明显的。因此对于那些对效率要求很高的算


法而言最好减少条件分支。
Gcc
提供了一些属性,用于

if
语句中判断这个
if
是有可能还是不大可能。比如内
存分配失败不大可能出现,可以写成
if (unlikely(NULL
== p))
表示
p==NULL
出现的概率很小,那么编译器就
会优化这段代码,让流水线的下一条指令是
if
外面的代
码。对于那些可能性很大的代码,则写成
if
(likely(NULL != p))
则流水线的下一条指令是
if
内部的代


(二)
如果内存不成问题或者有其他原因,尽可能用等
同于处理器位宽的类型。这个对于
mips

CPU
来说尤
为关键,一来影响访存,而来影响运算。从
PPC
的反
汇编来看,如果是两个
ushort_t
进行逻辑运算也需要扩
充成
32
位的进行,扩充的过程是需要浪费时间的。

(三)
能有
++i
的地方最好别用
i++
。当然这个大部分的
编译器可能会优化掉。
i++
的过程比
++i
要复杂多了,
i++
,需要先备份一下
i
,然后加
1
,再执行相应的操作















抱歉!评论已关闭.