计算机系统基础 第一章到第三章 知识点梳理
第一章 系统概述
这张图说的真的好,虽然和知识没有什么关系,最初看到也觉得没什么,但是学了大半学期再倒回来看这些话,我认为是十分正确的。同样的代码,不同的写法,效率可能差上成百上千倍。然后就是特别是通过bomb lab
,做了之后我对整个C语言的理解都不一样了。知其然,更要知其所以然,可能这就是系统的魅力吧!
为什么要学习计算机系统
- 为了编程序的时候少出错(逻辑性,准确性,内存溢出等)
- 为了在程序出错的时候很快找到出错的地方(gdb调试,或者直接查看汇编指令,找到错误)
- 为了明白程序是怎样在计算机上执行的(利用程序执行的原理,可以编写出更快的程序)
- 作为计算机专业的同学应该具有的底层意识。
计算机基本工作原理
一些基本的概念。
冯·诺依曼体系
冯·诺依曼结构最重要的思想是存储程序。
- 计算机的基本部件:控制器、存储器、运算器、输入设备和输出设备。
(计导必考题目!!) - 存储器不仅能存放数据,而且也能存放指令,形式上两者 没有区别,但计算机应能区分数据还是指令。
- 控制器应能自动取出指令来执行。
- 运算器应能进行加/减/乘/除四种基本算术运算,并且也 能进行一些逻辑运算和附加运算。
- 操作人员可以通过输入设备、输出设备和主机进行通信。
- 内部以二进制表示指令和数据。每条指令由操作码和地址码两部分组成。操作码指出操作类型,地址码指出操作数的地址。由一串指令组成程序。
- 采用“存储程序”工作方式。
计算机是如何工作的
图中的部件解释:
CPU
:中央处理器PC
:程序计数器MAR
:存储器地址寄存器ALU
:算术逻辑部件IR
:指令寄存器MDR
:存储器数据寄存器GPRs
:通用寄存器组(由若干通用寄存器组成,早期就是累加器)
程序执行前:
数据和指令事先存放在存储器中,每条指令和每个数据都有地址,指令按序存放,指令由OP
、ADDR
字段组成,程序起始地址置PC
注意:
- 程序启动前,指令和数据都存放在存储器中,形式上没有差别,都是0/1序列
- 采用”存储程序“工作方式:程序由指令组成,程序被启动后,计算机能自动取出一条一条指令执行,在执行过程中无需人的干预。
- 指令中需要给出的信息:
(1) 操作性质(操作码)
(2) 源操作数1 或/和源操作数2(立即数、寄存器编号、存储地址)
(3) 目的操作数地址(寄存器编号、存储地址)
开始执行程序:
- 第一步:根据
PC
取指令 - 第二步:指令译码
- 第三步:取操作数
- 第四步:指令执行
- 第五步:回写结果
- 第六步:修改
PC
的值 - 继续执行下一条指令
程序的开发和执行
机器语言
机器语言就是0/1代码啦!在计算机科学导论中我们也动手写了图灵机的操作(虽然我们班好像没有要求,但因为某种原因我写过几份,现在回想起来也挺有趣的)。感觉机器语言和图灵机有点类似,通过读写纸带和打孔之类的。
但是,如果要修改,或者打错了,那么就要从头再来,非常地崩溃,可读性也非常差。
汇编语言
- 用助记符表示操作码
- 用标号表示位置
- 用助记符表示寄存器
优点:
- 不会因为增减指令而需要修改其他指令
- 不需记忆指令编码,编写方便
- 可读性比机器语言强
高级语言
- 它们与具体机器结构无关
- 面向算法描述,比机器级语言描述能力强得多
- 高级语言中一条语句对应几条、几十条甚至几百条指令
- 有“面向过程”和“面向对象”的语言之分
- 处理逻辑分为三种结构:顺序结构、选择结构、循环结构
- 有两种转换方式:“编译”和“解释”
• 编译程序(Complier):将高级语言源程序转换为机器级目标程序,执行时只要启动目标程序即可,比如C++、C、Java等
• 解释程序(Interpreter ):将高级语言语句逐条翻译成机器指令并立即执行,不生成目标文件,比如Python等
源代码的处理
计算机的层次结构
计算机性能的定义
这部分在书上有,mooc上没有。其主要指标为吞吐率(throughput)
和响应时间(response time)
- 吞吐率:在单位时间内所完成的工作量。
- 响应时间:从作业提交开始到作业完成所用的时间。
第二章 数据的机器级表示与处理
数制和编码
- 十进制、二进制、十六进制、八进制……
- 定/浮点表示
数制
- 十进制(Decimal):结尾加
D
表示。 - 二进制(Binary):结尾加
B
表示。 - 八进制(Octal):结尾加
O
表示。 - 十六进制(Hexadecimal):结尾加
H
表示或者前缀加0x
表示。
进制转换:
整数部分:除基取余,上左下右
小数部分:乘基取整,上左下右
定点数和浮点数
计算机中只能通过约定小数点的位置来表示小数点
- 小数点位置约定在固定位置的数称为定点数
- 小数点位置约定为可浮动的数称为浮点数
实数的定义:
其中取值为或者,用来决定数的符号;是尾数,是阶/指数。是基数。
定点数的编码
原码、补码、移码、反码
- 原码:最高位的/表示正负,数值部分不变。但是缺点有:的表示不唯一,不利于编程;加减运算方式不统一;对硬件设计的要求较高;特别是当时候,实现比较困难。
- 补码(从50年代以来整数采用的编码方式):。同时补码实现了加和减的统一。
结论一:一个负数的补码等于模减该负数的绝对值
结论二:对于某一确定的模,某数减去小于模的另一数,总可以用该数加上另一数负数的补码来代替实现了加法和减法的统一。
变形补码:采用双符号位存放可能溢出的中间结果。
负数的补码的简便求法:从右至左遇到的第一个的前面各位取反(不含这个) - 移码:将每一个数值加上一个偏置常数
bias
。 - 反码:直接将二进制按位取反,一般没什么用。
C语言中的数
整数
其中整数类型分为带符号整数和无符号整数。
整数表示比较简单,大概熟悉C语言的程序员都掌握的比较好。
浮点数
浮点数的表示就很复杂了,编码上有许多细节。
首先是对科学计数法的概念:
- 浮点数的表示范围:
第0位数符S;第1-8位为8位移码表示阶码(偏置常数为128)
第9~31位为24位二进制原码小数表示的尾数。规格化尾数的小数点后第一位总是1,故规定第一位默认的“1”不明显表示出来。这样可用23个 数位表示24位尾数
最大正数:
最小正数:
- IEEE 754 标准(重点)
- 规格化数
Exponent | Significand | |
---|---|---|
1-254 | 任意小数点前隐含1 | |
0 | 0 | +/-0 |
255 | 0 | +/-inf |
255 | nonzero | NaN |
- 非规格化数
当 并且 时,用来表示非规格化数。
非数值表示
西文字符
二进制 | 十进制 | 十六进制 | 缩写 | 可以显示的表示法 | 名称/意义 |
---|---|---|---|---|---|
0000 0000 | 0 | 00 | NUL | ␀ | 空字符(Null) |
0000 0001 | 1 | 01 | SOH | ␁ | 标题开始 |
0000 0010 | 2 | 02 | STX | ␂ | 本文开始 |
0000 0011 | 3 | 03 | ETX | ␃ | 本文结束 |
0000 0100 | 4 | 04 | EOT | ␄ | 传输结束 |
0000 0101 | 5 | 05 | ENQ | ␅ | 请求 |
0000 0110 | 6 | 06 | ACK | ␆ | 确认回应 |
0000 0111 | 7 | 07 | BEL | ␇ | 响铃 |
0000 1000 | 8 | 08 | BS | ␈ | 退格 |
0000 1001 | 9 | 09 | HT | ␉ | 水平定位符号 |
0000 1010 | 10 | 0A | LF | ␊ | 换行键 |
0000 1011 | 11 | 0B | VT | ␋ | 垂直定位符号 |
0000 1100 | 12 | 0C | FF | ␌ | 换页键 |
0000 1101 | 13 | 0D | CR | ␍ | 归位键 |
0000 1110 | 14 | 0E | SO | ␎ | 取消变换(Shift out) |
0000 1111 | 15 | 0F | SI | ␏ | 启用变换(Shift in) |
0001 0000 | 16 | 10 | DLE | ␐ | 跳出数据通讯 |
0001 0001 | 17 | 11 | DC1 | ␑ | 设备控制一(XON 启用软件速度控制) |
0001 0010 | 18 | 12 | DC2 | ␒ | 设备控制二 |
0001 0011 | 19 | 13 | DC3 | ␓ | 设备控制三(XOFF 停用软件速度控制) |
0001 0100 | 20 | 14 | DC4 | ␔ | 设备控制四 |
0001 0101 | 21 | 15 | NAK | ␕ | 确认失败回应 |
0001 0110 | 22 | 16 | SYN | ␖ | 同步用暂停 |
0001 0111 | 23 | 17 | ETB | ␗ | 区块传输结束 |
0001 1000 | 24 | 18 | CAN | ␘ | 取消 |
0001 1001 | 25 | 19 | EM | ␙ | 连接介质中断 |
0001 1010 | 26 | 1A | SUB | ␚ | 替换 |
0001 1011 | 27 | 1B | ESC | ␛ | 跳出 |
0001 1100 | 28 | 1C | FS | ␜ | 文件分割符 |
0001 1101 | 29 | 1D | GS | ␝ | 组群分隔符 |
0001 1110 | 30 | 1E | RS | ␞ | 记录分隔符 |
0001 1111 | 31 | 1F | US | ␟ | 单元分隔符 |
0111 1111 | 127 | 7F | DEL | ␡ | 删除 |
|
|
|
汉字
- 汉字的区位码:由94行、94列组成,行号为区号,列号为位号。
- 汉字的国标码:将区号和位号各自加上32(20H)。国标码中区号和位号各占7位。在计算机内部,为方便处理与存储,前面添一个0,构成一个字节。
- 至少需要2个字节才能表示一个汉字内码(因为汉字总数超过6万个)
数据宽度和储存容量
容量单位
中文 | 表示 | 转化 |
---|---|---|
千字节 | KB | |
兆字节 | MB | |
千兆字节 | GB | |
兆兆字节 | TB |
通信中的带宽单位
中文 | 表示 | 转化 |
---|---|---|
千比特/秒 | kb/s | |
兆比特/秒 | Mb/s | |
千兆比特/秒 | Gb/s | |
兆兆比特/秒 | Tb/s |
注意:容量单位和带宽单位里面明显大小写有一些不一样,因为表示的含义不同,一个是1000,一个1024.
C语言中数据类型的宽度
C声明 | 典型32位机器(单位:字节) | 典型64位机器(单位:字节) |
---|---|---|
char | 1 | 1 |
short int | 2 | 2 |
int | 4 | 4 |
long int | 4 | 8 |
char* | 4 | 8 |
float | 4 | 4 |
double | 8 | 8 |
long double 的确切精度没有规定,所以对于不同平台来说,有的是8字节,有的是10字节,有的是12字节或者16字节。
数据的存储和排列顺序
80年代开始,几乎所有通用计算机都采用字节编址。在高级语言中声明的基本数据类型有char、short、int、long、long long、float、double、long double等各种不同长度数据,一个基本数据可能会占用多个存储单元。所以有一些问题我们还需要考虑,比如说:变量的地址是其最大地址还是最小地址?多个字节在存储单元中存放的顺序如何?
大/小端方式
测试代码:
1 |
|
在我的机器上运行的结果:
大小端非常重要,因为在看指令的时候,大小端不同,表示的值就不同。
注意:IA-32采用的是小端方式。
数字逻辑电路
布尔代数
0 | 0 | 0 | 0 | 1 | 0 |
0 | 1 | 0 | 1 | 1 | 1 |
1 | 0 | 0 | 1 | 0 | 1 |
1 | 1 | 1 | 1 | 0 | 0 |
全加器(一位加法器)
两个加数为和,低位进位为Cin
,和为,向高位的进位为Cout
。
化简之后,逻辑表达式如下:
n位加法器
n位加法器可用n个全加器实现
无符号整数加
带符号整数加
- 溢出标志OF:
- 符号标志SF:
- 零标志ZF:当且仅当
- 进位/借位标志CF:
n位整数加/减运算器
- 当为时,做减法。
- 当为时,做加法。
算术逻辑部件ALU
:
数据的运算
高级语言程序中涉及的运算(以C语言为例)
- 整数算术运算、浮点数算术运算
- 按位、逻辑、移位、位扩展和位截断等运算
指令集中涉及到的运算:
涉及到的定点数运算
:
算术运算:
- 带符号整数:取负/ 符号扩展/ 加/ 减/ 乘/ 除 / 算术移位
- 无符号整数:0扩展/ 加/ 减/ 乘/ 除/ 逻辑左移/ 逻辑右移
逻辑运算:
- 逻辑操作:与/ 或/ 非/ …
涉及到的浮点数运算
:加、减、乘、除
指令中的运算操作在运算电路中进行
- 基本运算部件ALU、通用寄存器组,以及其他部件
整数的加减运算
- 计算机中所有运算都基于加法器实现。
- 加法器不知道所运算的是带符号数还是无符号数。
- 加法器不判定对错,总是取低n位作为结果,并生成标志信息。
无符号数
加法:
- 当
- 当
减法:
- 当
- 当
带符号数
加法:
- 当,负溢出
- 当,正常
- 当,正溢出
减法:
- 当,负溢出
- 当,正常
- 当,正溢出
第三章 程序的转换及机器级表示
概述
机器级指令和汇编指令一一对应,都是机器级指令。
- 机器指令是一个0/1序列,由若干字段组成。
- 汇编指令是机器指令的符号表示,在不同机器上格式可以不同
比如说:Intel格式:mov [bx+di-6],cl
AT&T格式:movb %cl, -6(%bx,%di)
这两条指令的功能其实都是表示M[R[bx]+R[di]-6]←R[cl]
,其中R
表示的是寄存器内容,M
表示的是存储单元内容。
IA-32 指令系统
概述
寄存器组织
编号 | 8位寄存器 | 16位寄存器 | 32位寄存器 | 64位寄存器 | 128位寄存器 |
---|---|---|---|---|---|
000 | AL | AX | EAX | MM0/ST(0) | XMM0 |
001 | CL | CX | ECX | MM1/ST(1) | XMM1 |
010 | DL | DX | EDX | MM2/ST(2) | XMM2 |
011 | BL | BX | EBX | MM3/ST(3) | XMM3 |
100 | AH | SP | ESP | MM4/ST(4) | XMM4 |
101 | CH | BP | EBP | MM5/ST(5) | XMM5 |
110 | DH | SI | ESI | MM6/ST(6) | XMM6 |
111 | BH | DI | EDI | MM7/ST(7) | XMM7 |
字长不断扩充,指令保持兼容,ST(0)
- ST(7)
是80位,MM0
- MM7
使用其低64位
IA-32中寄存器名称的含义
:
CS
—— 代码段寄存器(Code Segment)SS
—— 栈段寄存器(Stack Segment)DS
—— 数据段寄存器(Data Segment)ES
—— 扩展段寄存器(Extended Segment)FS,GS
—— 数据段寄存器EAX
—— 累加寄存器(Extended Accumulator Register)ECX
—— 计数寄存器(Extended Counter Register)EDX
—— 数据寄存器(Extended Data Register)EBX
—— 指向数据段(DS)的指针(Extended Base Register)ESI
—— 指向数据段(DS)的指针,表示字符串操作的源(Extended Source Index)EDI
—— 指向数据段(ES)的指针,表示字符串操作的目标(Extended Destination Index)EBP
—— 指向栈(SS)的指针,一般表示当前栈帧的底部(Extended Base Pointer)ESP
—— 指向栈(SS)顶的指针(Extended Stack Pointer)EIP
—— 指令寄存器(Enhanced Instruction Pointer)EFLAG
—— 标志位寄存器
标志寄存器
- 6个条件标志
OF
、SF
、ZF
、CF
,功能在前文已经介绍,这里不再赘述AF
:辅助进位标志(在BCD码运算时才有意义)PF
:奇偶标志
- 3个控制标志
DF
(Direction Flag):方向标志(自动变址方向是增还是减)IF
(Interrupt Flag):中断允许标志(仅对外部可屏蔽中断有用)TF
(Trap Flag):陷阱标志(是否是单步跟踪状态)
保护模式下的寻址方式
寻址方式 | 说明 |
---|---|
立即寻址 | 指令直接给出操作数 |
寄存器寻址 | 指定的寄存器R的内容为操作数 |
位移 | |
基址寻址 | |
基址加位移 | |
比例变址加位移 | |
基址加变址加位移 | |
基址加比例变址加位移 | |
相对寻址 | 跳转目标指令地址 |
注:LA
:线性地址,(X)
:X的内容,SR
:段寄存器,PC
:程序计数器,R
:寄存器,A
:指令中给定地址段的位移量,B
:基址寄存器,I
:变址寄存器,S
:比例系数
- 举例说明:
1 | int x; |
- 如何计算
a[i]
的地址?
,当时, - 如何计算
b[i][j]
的地址?
,当时, - 如何计算
d[i]
的地址?
,当时,
机器指令格式
指令类型
传送指令
1.通用数据传送指令
MOV
:一般传送,包括movb
、movw
和movz
等MOVS
:符号扩展传送,如movsbw
、movswl
等MOVZ
:零扩展传送,如movzwl
、movzbl
等XCHG
:数据交换PUSH/POP
:入栈/出栈,如pushl
、pushw
、popl
、popw
等
2.地址传送指令
LEA
:加载有效地址,如leal (%edx,%eax), %eax
的功能为R[eax]←R[edx]+R[eax]
,执行前,若R[edx]=i,R[eax]=j
,则指令执行后,R[eax]=i+j
3.输入输出指令
IN
和OUT
:I/O端口与寄存器之间的交换
4.标志传送指令
PUSHF
、POPF
:将EFLAG
压栈,或将栈顶内容送EFLAG
入栈 pushw %ax
- 栈(Stack)是一种采用“先进后出”方式进行访问的一块存储区,用于嵌套过程调用。从高地址向低地址增长
- “栈”不等于“堆栈”(由“堆”和“栈”组成)
出栈 popw %ax
定点算术指令
- 加/减运算(影响标志、不区分无/带符号)
ADD
:加,包括addb
、addw
、addl
等SUB
:减,包括subb
、subw
、subl
等 - 增1/减1运算(影响除CF以外的标志、不区分无/带符号)
INC
:加,包括incb
、incw
、incl
等DEC
:减,包括decb
、decw
、decl
等 - 取负运算(影响标志、若对0取负,则结果为0且CF清0,否则CF置1)
NEG
:取负,包括negb
、negw
、negl
等 - 比较运算(做减法得到标志、不区分无/带符号)
CMP
:比较,包括cmpb
、cmpw
、cmpl
等 - 乘/除运算(不影响标志、区分无/带符号)
MUL
/IMUL
:无符号乘/带符号乘DIV
/IDIV
:带无符号除/带符号除
1.乘法指令:
- 指令中只给出一个操作数
SRC
:则另一个源操作数隐含在AL
/AX
/EAX
中,将SRC
和累加器内容相乘,结果存放在AX
(16位)或DX-AX
(32位)或EDX-EAX
(64位)中。DX-AX
表示32位乘积的高、低16位分别在DX
和AX
中。
得到的结果的位数: - 指令中给出两个操作数
DST
和SRC
,则将DST
和SRC
相乘,结果在DST
中。
得到的结果的位数: - 指令中给出三个操作数
REG
、SRC
和IMM
,则将SRC
和立即数IMM
相乘,结果在REG
中。
得到的结果的位数:
2.除法指令
只明显指出除数,用EDX-EAX
中内容除以指定的除数
- 若为8位,则16位被除数在
AX
寄存器中,商送回AL
,余数在AH
- 若为16位,则32位被除数在
DX-AX
寄存器中,商送回AX
,余数在DX
- 若为32位,则被除数在
EDX-EAX
寄存器中,商送EAX
,余数在EDX
按位运算
1.移位运算(左/右移时,最高/最低位送CF)
SHL
/SHR
: 逻辑左/右移,包括shlb
、shrw
、shrl
等SAL
/SAR
: 算术左/右移,左移判溢出,右移高位补符(移位前、后符号位发生变化,则OF
=1 )包括salb
、sarw
、sarl
等ROL
/ROR
: 循环左/右移,包括rolb
、rorw
、roll
等RCL
/RCR
: 带进位循环左/右移,即:将CF
作为操作数 一部分循环移位,包括rclb
、rcrw
、rcll
等
注:sarw $1,%ax
可简写成sarw %ax
2.逻辑运算
NOT
:非,包括notb
、notw
、notl
等AND
:与,包括andb
、andw
、andl
等OR
:或,包括orb
、orw
、orl
等XOR
:异或,包括xorb
、xorw
、xorl
等TEST
:做“与”操作测试,仅影响标志
仅NOT
不影响标志,其他指令OF
=CF
=0,而ZF
和SF
则根据结果设置:若全0,则ZF
=1;若最高位为1,则SF
=1
控制转移指令
指令执行可按顺序或跳转到转移目标指令处执行
- 无条件转移指令
JMP DST
:无条件转移到目标指令DST
处执行 - 条件转移
Jcc DST
:cc
为条件码,根据标志(条件码)判断是否满足条件,若满足,则转移到目标指令DST
处执行,否则按顺序执行 - 条件设置
SETcc DST
:按条件码cc
判断的结果保存到DST
(是一个8位寄存器) - 调用和返回指令(用于过程调用)
CALL DST
:返回地址RA
入栈,转DST
处执行RET
:从栈中取出返回地址RA
,转到RA
处执行 - 中断指令
这里我们可以注意到,调用的函数返回地址是RA
,这非常重要
C语言程序的机器级表示
过程调用的机器级表示
过程调用的执行步骤
过程调用的执行步骤(P
为调用者,Q
为被调用者)
P
将入口参数(实参)放到Q
能访问到的地方;P
保存返回地址,然后将控制转移到Q
; (CALL指令)Q
保存P
的现场,并为自己的非静态局部变量分配空间;- 执行
Q
的过程体(函数体); Q
恢复P
的现场,释放局部变量空间;Q
取出返回地址,将控制转移到P
。(RET指令)
IA-32的寄存器使用约定
- 调用者保存寄存器:
EAX
、EDX
、ECX
当过程P
调用过程Q
时,Q
可以直接使用这三个寄存器,不用将它们的值保存到栈中。如果P
在从Q
返回后还要用这三个寄存器的话,P
应在转到Q
之前先保存,并在从Q
返回后先恢复它们的值再使用。 - 被调用者保存寄存器:
EBX
、ESI
、EDI
Q
必须先将它们的值保存到栈中再使用它们,并在返回P
之前恢复它们的值。 EBP
和ESP
分别是帧指针寄存器和栈指针寄存器,分别用来指向当前栈帧的底部和顶部。
注意:
- 所以为了减少准备和结束阶段的开销,每个过程应该先使用
EAX
、ECX
、EDX
,这样就不用存栈。 - 返回参数总在
EAX
中
过程(函数)的结构
一个C过程的大致结构如下:
准备阶段:
- 形成帧底:
push
指令 和mov
指令 - 生成栈帧(如果需要的话):
sub
指令或and
指令 - 保存现场(如果有被调用者保存寄存器):
mov
指令(为什么?因为所有过程共享一套GPRs)
过程(函数)体:
- 分配局部变量空间,并赋值
- 具体处理逻辑,如果遇到函数调用时,
(1) 准备参数:将实参送栈帧入口参数处
(2)CALL
指令:保存返回地址并转被调用函数 - 在
EAX
中准备返回参数
第一个入口参数在EBP+8
的位置(EBP
存EBP
在Caller
中的旧值,EBP+4
存返回地址)
结束阶段:
- 退栈:
leave
指令或pop
指令 - 取返回地址返回:
ret
指令
选择和循环语句
1.switch-case语句
1 | movl 8(%ebp),%eax |
跳转表在目标文件的只读节中,按4字节边界对齐。
2.循环结构与递归结构的比较
循环结构一般使用的都是简单变量,直接保存到寄存器中。而递归结构每次都要分配栈空间,时间和空间的开销都非常得大。(比如递归写法的快速幂和迭代写法的快速幂,速度会差几十倍至几十倍(取决于数据范围)。所以,为了提高程序性能,能使用非递归方式执行则最好用非递归结构。
复杂数据类型的分配和访问
数组
数组定义 | 数组名 | 数组元素类型 | 数组元素大小(B) | 数组大小(B) | 起始地址 | 元素i的地址 |
---|---|---|---|---|---|---|
char S[10] | S | char | 1 | 10 | &S[0] | &S[0]+i |
char* SA[10] | SA | char* | 4 | 40 | &SA[0] | &SA[0]+4*i |
double D[10] | D | double | 8 | 80 | &D[0] | &D[0]+8*i |
double* DA[10] | DA | double* | 4 | 40 | &DA[0] | &DA[0]+4*i |
数组元素和指针变量的表达式计算示例:
序号 | 表达式 | 类型 | 值的计算方式 | 汇编代码 |
---|---|---|---|---|
1 | A | int* | SA | leal (%ecx),%eax |
2 | A[0] | int | M[SA] | movl (%ecx),%eax |
3 | A[i] | int | M[SA+4*i] | movl (%ecx,%edx,4),%eax |
4 | &A[3] | int* | SA+12 | leal 12(%ecx),%eax |
5 | &A[i]-A | int | (SA+4*i-SA)/4=i | movl %edx,%eax |
6 | *(A+i) | int | M[SA+4*i] | movl (%ecx,%edx,4),%eax |
7 | *(&A[0]+i-1) | int | M[SA+4*i-4] | movl -4(%ecx,edx,4),%eax |
8 | A+i | int* | SA+4*i | leal (%ecx,%edx,4),%eax |
指针数组和多维数组:
- 由若干指向同类目标的指针变量组成的数组称为指针数组。
其定义的一般形式如下:
存储类型 数据类型 *指针数组名[元素个数]
例如,int *a[10];
定义了一个指针数组a
,它有10个元素,每个元素都是一个指向int型数据的指针。 - 一个指针数组可以实现一个二维数组
结构体和联合体
结构体成员在内存的存放和访问
- 分配在栈中的auto结构型变量的首地址由
EBP
或ESP
来定位 - 分配在静态区的结构型变量首地址是一个确定的静态区地址
- 结构型变量 x 各成员首址可用“基址加偏移量”的寻址方式
结构体数据作为入口参数:
首先,当结构体变量需要作为一个函数的形参时,形参和调用函数中的实参应具有相同结构。
然后,其传递有两种方式:
- 若采用按值传递,则结构成员都要复制到栈中参数区,这既增加时间开销又增加空间开销,且更新后的数据无法在调用过程使用
- 通常应按地址传递,即:在执行CALL指令前,仅需传递指向结构体的指针而不需复制每个成员到栈中
联合体数据的分配和访问
联合体各成员共享存储空间,按最大长度成员所需空间大小为目标
注:联合体通常用于特殊场合,如,当事先知道某种数据结构中的不同字段的使用时间是互斥的,就可将这些字段声明为联合,以减少空间。但有时会得不偿失,可能只会减少少量空间却大大增加处理复杂性。
数据的对齐存放
为什么要对齐
各种不同长度的数据存放时,有两种处理方式:
- 按边界对齐(若一个字为32位)
字地址:4的倍数(低两位为0)
半字地址:2的倍数(低位为0)
字节地址:任意 - 不按边界对齐
坏处:可能会增加访存次数
对齐方式的设定
#pragma pack(n)
- 为编译器指定结构体或类内部的成员变量的对齐方式。
- 当自然边界(如int型按4字节、short型按2字节、float按4字节)比n大时,按n字节对齐。
- 缺省或
#pragma pack()
,按自然边界对齐。
__attribute__((aligned(m)))
- 为编译器指定一个结构体或类或联合体或一个单独的变量(对象)的对齐方式。
- 按m字节对齐(m必须是2的幂次方),且其占用空间大小也是m的整数倍,以保证在申请连续存储空间时各元素也按m字节对齐。
__attribute__((packed))
- 不按边界对齐,称为紧凑方式。
越界访问和缓冲区溢出
先看一个例子直观感受:
当i>1
的时候,就出现了问题,因为数组访问越界了,影响到了其他位置的值甚至直接爆栈。
C语言程序中对数组的访问可能会有意或无意地超越数组存储区范围而无法发现。
数组存储区可看成是一个缓冲区,超越数组存储区范围的写入操作
称为缓冲区溢出。
例如,对于一个有10个元素的char型数组,其定义的缓冲区有10个字节。若写一个字符串到这个缓冲区,那么只要写入的字符串多于9个字符(结束符‘\0’占一个字节),就会发生“写溢出”。
带来的破坏:
- 缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、
应用软件中广泛存在。 - 缓冲区溢出攻击是利用缓冲区溢出漏洞所进行的攻击。利用缓冲区
溢出攻击,可导致程序运行失败、系统关机、重新启动等后果。
x86-64 体系
x86-64操作数格式
操作数的寻址有如下方式:
- 立即数,用来表示常数值,书写方式为’$'加标准C表示法表示的整数,如$-577或$0x1F。
- 寄存器,
R[ra]
表示ra
寄存器的内容。 - 内存引用,
M[Addr]
表示地址为Addr
的存储单元内的值
立即数Imm
、基址寄存器rb
、变址寄存器ri
、比例因子s
(其值为1、2、4、8)。
类型 | 格式 | 操作数值 | 名称 |
---|---|---|---|
立即数 | $Imm | Imm | 立即数寻址 |
寄存器 | ra | R[ra] | 寄存器寻址 |
存储器 | Imm | M[Imm] | 绝对寻址 |
存储器 | (ra) | M[R[ra]] | 间接寻址 |
存储器 | Imm(rb) | M[Imm+R[rb]] | (基址+偏移量)寻址 |
存储器 | (rb,ri) | M[R[rb]+R[ri]] | 变址寻址 |
存储器 | Imm(rb,ri) | M[Imm+R[rb]+R[ri]] | 变址寻址 |
存储器 | (,ri,s) | M[R[ri]*s] | 比例变址寻址 |
存储器 | Imm(,ri,s) | M[Imm+R[ri]*s] | 比例变址寻址 |
存储器 | (rb,ri,s) | M[R[rb]+R[ri]*s] | 比例变址寻址 |
存储器 | Imm(rb,ri,s) | M[Imm+R[rb]+R[ri]*s] | 比例变址寻址 |
x86-64寄存器
X86-64有16个64位寄存器,分别是:rax
,rbx
,rcx
,rdx
,esi
,edi
,rbp
,rsp
,r8
,r9
,r10
,r11
,r12
,r13
,r14
,r15
。其中:
rax
作为函数返回值使用。rsp
栈指针寄存器,指向栈顶rdi
,rsi
,rdx
,rcx
,r8
,r9
用作函数参数,依次对应第1参数,第2参数……rbx
,rbp
,r12
,r13
,r14
,r15
用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改r10
,r11
用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
注:
- 最多有六个整形或指针型参数通过寄存器传递,超过六个只能通过栈来传递(所以在传递参数的时候尽量保证传递的参数个数在6个以内,如果是数组就用传递指针,这样可以提高程序的运行效率)
- 在函数中尽量使用
rax
,r10
,r11
,若使用rbx
,rbp
,r12
,r13
,r14
,r15
则需要将它们先保存在栈中在使用,最后返回前再恢复其值
63~0位 | 31~0位 | 15~0位 | 7~0位 | 用途 |
---|---|---|---|---|
%rax | %eax | %ax | %al | 返回值 |
%rbx | %ebx | %bx | %bl | 被调用者保存 |
%rcx | %ecx | %cx | %cl | 第4个参数 |
%rdx | %edx | %dx | %dl | 第3个参数 |
%rsi | %esi | %si | %sil | 第2个参数 |
%rdi | %edi | %di | %dil | 第1个参数 |
%rbp | %ebp | %bp | %bpl | 被调用者保存 |
%rsp | %esp | %sp | %spl | 栈指针 |
%r8 | %r8d | %r8w | %r8b | 第5个参数 |
%r9 | %r9d | %r9w | %r9b | 第6个参数 |
%r10 | %r10d | %r10w | %r10b | 调用者保存 |
%r11 | %r11d | %r11w | %r11b | 调用者保存 |
%r12 | %r12d | %r12w | %r12b | 被调用者保存 |
%r13 | %r13d | %r13w | %r13b | 被调用者保存 |
%r14 | %r14d | %r14w | %r14b | 被调用者保存 |
%r15 | %r15d | %r15w | %r15b | 被调用者保存 |