首页 > 代码库 > C语言杂记 -- 简陋的<深入理解计算机系统>笔记

C语言杂记 -- 简陋的<深入理解计算机系统>笔记

 

程序的表示

3264位操作系统是由CPU寄存器的位数决定,即虚拟寻址的范围为2^322^64

字节的大端小端法是以字节为基本单位的:比如十进制的7在十六位机器上表示

·          

地址

100

101

大端法

00000000

00000111

小端法

00000111

00000000

 

大部分编译器默认进行算数右移和用补码表示复数

对于移位k来说,实际移位量为k mod 2^ww为所移动数据类型的位数)

对于整型常量来说c编译器都是先将其看成正数,如果前面有 - 运算符则对其去负即转换成补码表示。

//这造成了C中及其隐晦的存在

//limits.h

#define INT_MAX 2147483647   // 二进制表示方法为0111 1111 1111 1111,为C最大表示的整型数2^31 - 1

#define INT_MIN -INT_MAX - 1)   

/*

为什么不直接写成-2147483648呢? 这是因为编译器在看到这个常量时,不会先看取负元算符,而是看到了21474836482^31,编译器会感觉到无法用int型来表示,便会将其用长整形来表示为unsigned int 1000 0000 0000 0000,这样它的类型就为unsigned,在你进行使用的时候便会发生错误,考虑如下代码

*/

#include <limits.h>

#include <stdio.h>

#include <stdlib.h>

 

int main()

{

    int a = 5;

 

    printf("%d", a > -2147483648); // result is 0, 这是因为比较过程中发生了隐式转换,转换成了unsigned int 之间的比较。

    printf("%d", a > INT_MIN);     // result is 1,两者都是int类型,所以比较成功


    return 0;

}

这就是C语言中,负数最小值为-2147483648,却不能直接写出的原因。具体请参考http://www.cnblogs.com/Jack47/archive/2013/01/06/TMin32-in-c.html

浮点数:

32位浮点数由一位符号位s8位阶码exp即权值,23位尾数

数值类型

s

exp

尾数f

规格化

s

!=0 && != 255

f(尾数定义为1+f), 阶码为exp - 01111111

非规格化

s

0000 0000

尾数为f(尾数定义为f, exp1 - 01111111)原因见下

无穷大

s1,0分别表示正负无穷大)

1111 1111

000000000000000.......

NaN(NOT a NUMBER)

s

1111 1111

!=0

 

原因保证最小规格化数到最大规格化数的平稳过渡,这样最小规格化数的exp0000 0001 - 0111 1111 = -126;最大非规格化数的exp1 - 0111 1111 = -126. 这样就保证了二进制小数数值均匀的接近于零

64位浮点数一位符号位s, 11位阶码, 52位尾数, 原理相同.

浮点数的舍入(多采用向偶数舍入, 又称其为向最接近的值舍入): 当值不位于要舍入的中间值时, 向最近的整数舍入; 当值为中间值时, 向最接近的偶数舍入. 比如1.5舍入为整数, 因为其位于一和二中间, 向偶数舍入, 2.

汇编指令(ATT格式)

知识准备:32位系统寄存器

名称

特殊作用

  

%eax

通常作为函数返回值,

低十六位组成%ax, %ax中高八位为%ah,低八位为%al

%ecx

 

同上

%edx

 

同上

%ebx

 

同上

%esi

 

只有低十六位的

%edi

 

同上

%esp

栈指针

同上

%ebp

帧指针

同上

 

操作数(R表示取寄存器的值, M(addr)表示对地址addr的引用

类型

格式

操作数值

例子

立即数

$imm

imm

push $imm: 入栈imm

寄存器

%e

R[e]

push %eax: 寄存器%eax的值入栈

存储器

(imm)

M[imm]

push (4): 地址4处的值入栈

存储器

(%e)

M[R[e]]

push (%eax): %eax的值为地址的值入栈

存储器

imm(%e)

M[R[e] + imm]

...

存储器

(Ea, Eb)

M[R[Ea]+ R[Eb]]

...

存储器

imm(Ea, Eb)

M[R[Ea]+ R[Eb] + imm]

...

存储器

(, Ei, s)

M[R[Ei] * s]

...

存储器

(Eb, Ei, s)

M[R[Eb]+ R[Ei] * s]

...

存储器

imm(Eb, Ei, s)

M[R[Eb]+ R[Ei] * s + imm]

最常用

 

数据传送指令:MOV(S, Z), SD不能同时为存储器目标

Mov[b, w, l]:分别传送[字节, , 双字],

Movb S, D: S传送到D

Movs[bw, bl, wl]: 符号扩展

Movz[bw, bl, wl]: 零扩展

Pushl: 双字入栈, eg, push %eax

Popl: 双字出栈, eg, pop %eax

算术和逻辑操作

Leal S, D: &SàD

INC D: D++

DEC D: D--

DEG D: -D

NOT D: ~D (取补)

ADD S, D: D = D + S

SUB S, D: D = D - S

IMUL S, D: D = S * D

XOR S, D: D = D ^ S (异或)

OR S, D: D = D | S

AND S, D: D = D & S

SAL(SHL) k, D: D = D << k

SAR k, D: D = D >> k (算数右移)

SHR k, D: D = D >> k (逻辑右移)

特殊的算数操作

imull S: 某个寄存器 = S * R[%eax] (有符号)

mull S: (无符号)

cltd S: R[%eax] 进行符号扩展到某个寄存器

idivl S: 有符号除法, R[%eax] / S; 商放到%eax, 余数放到%edx

divl S: 无符号除法

控制:条件, 循环

条件(根据条件码来设置)

cmp[b, w, l]: 分别比较字节, , 双字.   cmp S1 S2(基于 S2 - S1), 会设置条件码

set[e(equal), n(not), s(负数),  g(great), l(less), a(above), b(below)] D: 根据条件码集合将D设置为10.

test S1, S2(基于S1 & S2, 只改变条件码, 通常用来测试S1 ?= S2)

j[…set相同, 额外有mp]: jmp LABLE(直接跳转到LABLE), jmp *op(间接跳转到对op解引用后, 以解引用后的值进行跳转)

条件,循环控制多借助于上述组合来使用:eg

test %edx, %eax

je .L3   如果相等则跳转到.L3

.L3

    伪代码

cmp %edx, %eax

jne .L3   如果不相等则跳转到.L3

.L3

    伪代码

条件传送指令: 有利于现代处理器更好的执行, 避免分支惩罚

类似于 x < y ? y - x : x - y, 语句可以产生条件传送指令

配合上文的条件码, cmov[set相同], cmovl S, R 若小于则传送

switch语句配合跳转表来使用, 只进行一次条件判断

call Label(* Operand): 过程调用, 将返回地址入栈, 并跳转到被调用过程的起始地址

ret: call指令作用相反

优化程序性能

消除循环的低效率

对于循环中的过程调用尽量移出循环外, 例如:

for (i = 0; i < strlen(s); i++) //strlen()函数为线性增长,在字符串长度很大时,很消耗系统资源

减少不必要的存储器引用, 将存储器引用储存在临时变量中.

处理器优化: 即充分利用存储器流水线操作的吞吐量

循环展开, 减少读写相关, 即所使用的数据必须等待上一次操作完成.

重新结合变换, 减少读写相关, eg

for ()

    acc = (acc OP data[i]) OP data[i + 1]

    // acc = acc OP (data[i] OP data[ i + 1]    相比上一行, 处理器可以更新acc的同时进行(data[i] OP data[i + 1]), 而上一行就必须等待acc更新完之后才能进行(acc OP data[i])操作.

利用局部性原理: 高速缓存

时间局部性: 被引用过一次的存储器位置可能在不远的时间内继续被引用

空间局部性: 如果一个存储器位置被引用了, 很可能在不远的将来被引用其附近的存储器位置

每一级缓存只是关心其上下级缓存的分组情况, 缓存管理可能是硬件本身, 或者软件, 亦或是二者的结合

 

内核为每个进程维护一个描述符表, 描述符表(每个进程独有)是一个结构指针数组, 每个指针指向打开文件表, 打开文件表(包含当前文件位置, 引用计数等, 所有进程共享)包含一个指向v_node表的指针, v_node表也是包含一些文件基本信息(所有进程共享). 值得注意的是, 内核默认为每个进程打开标准输出, 标准输入, 标准错误输出三个文件, 如果任何一个打开文件表的引用计数变为零, 内核则会收回打开文件表中此文件的内存, 在对其引用可能会发生意想不到的错误. 子进程与父进程不同且继承的是描述符表, 同时系统借助描述符表和虚拟存储器来实现文件共享.