一 C语言程序举例
Intro
① ISO C90标准,32位系统的程序运行:
- -2147483648 < 2147483648 False
- i = -2147483648; 2147483648 > i; True
- -2147483648-1 < 2147483648 True
Why?如何理解呢?
- 编译器如何处理字面量
- 高级语言中的运算规则
- 高级语言与指令之间的对应
- 机器指令的执行过程
- 机器级数据的表示和运算
- …
② x, y都为int型,当x = 65535, y = x*x; y的值是多少?
- y = -131071 WHY?
③ x, y依旧为int型,对于(x>y) == (-x < -y)是否成立?
- False; 当
x = -2147483648
时,y任意(除-2147483648
)时都不成立 WHY?
以上说明计算机的世界和现实世界是有一定差距滴~
1.1 C举例
//----①----
int a = 0x8000000;
int b = a/-1;
printf("b = %d\n", b);
//运行结果:b = -2147483868
//----------------------
//----②----
int b = -1;
int c = a/b;
printf("c = %d\n", c);
//报错 ---> Floating Point Exception,CPU检测到了溢出异常
Rea:
- 关于①:由于
a
−
1
\frac{a}{-1}
−1a中,
-1
为常数,在这个过程中程序得知除以-1
,其被优化成取负指令neg
,因此没有发生除法溢出【检测不到】【可以使用objdump反汇编编码来检测】 - 关于②:因为
a、b
都为变量,在进行运算时采用的是除法指令IDIV
实现,但它不生成OF
指令,那么是如何判断溢出异常的呢?
这个实际上是 “除法错”异常#DE(类型0),在Linux中,对#DE类型发SIGFPE 信号
1.2 什么是计算机系统
将计算机系统分为两个部分,四类对象进行了解
-
软件
使用方式 操作对象 应用(问题) 最终用户 算法 程序员 编程语言【数据的机器级表示、运算,过程的机器级语句和调用】 程序员 操作系统/虚拟机【编译和链接的部分内容】 程序员 -
软硬结合
使用方式 操作对象 指令集体系结构(ISA)和汇编层【指令系统、机器代码、汇编语言】 架构师 -
硬件
使用方式 操作对象 指令集体系结构(ISA) 架构师 微体系结构【CPU的通用结构及层次结构存储系统】 架构师 功能部件 架构师 电路 电子工程师 器件 电子工程师 -
程序的执行结果依赖于这些所有的使用方式,要解决问题需要把所有层次连接起来
1.3 冯·诺依曼思想
第一台计算机咋来的咧~第一台存储计算机【EDSAC】
重要思想:存储程序(Stroed-program)
工作方式:将计算机要完成的工作编写成程序,然后将程序和原始数据送入主存并启动执行。一旦程序被启动,计算机能在不需操作人员的干预下,自动完成逐条取出指令和实现指令的任务
现在基本上所有的通用计算机大都采用冯·诺伊曼结构
1.3.1 结构思想
总结,冯·诺伊曼结构为:
- 有主存,用来存放程序和数据
- 有自动逐条取出指令的部件
- 有具体执行指令(即运算) 的部件
- 程序由指令构成
- 指令描述如何对数据进行处理
- 输入计算机的部件应该有程序和原始数据
- 需要有将运算结果输出计算机的部件
1.3.2 结构主要思想
-
计算机组成部分
1 2 3 4 5 运算器 控制器 存储器 输入设备 输出设备 -
基础部件功能
部件 功能 存储器 能存放数据和指令【形式上都为 0-1
序列】,且计算机能分辨出来控制器 能自动取出指令来执行 运算器 应能进行四则运算,和一些逻辑运算及附加运算 输入、输出设备 操作人员能通过输入、输出设备与主机通信 -
内部以二进制表示指令和数据
每条指令码由操作码和地址码构成【
0-1
序列】。操作码指出操作类型,地址码指出操作数的地址。可以看出,程序由一串指令组成 -
采用“存储程序”的工作方式
1.4 现代计算机结构模型
基本概念:
CPU: Computer Processing Unit【算术处理单元】
MAR: Memory Address Register【存储器地址寄存器】
MDR: Memory Data Register【存储器数据寄存器】
ALU: Arithmetic Logic Unit 【算术逻辑单元】
IR: Instructor Register 【指令寄存器】
PC: Program Counter 【程序计数器】【CPU中的寄存器】
1.4.1 现实举例
可以把这个比为现实的饭店/工厂。举个栗子:妈妈做菜
关于这个过程,我们可以把各部分作为:
做菜工具 | 计算机结构 |
---|---|
厨房 | CPU |
妈妈 | 控制器【拿到菜单开始做菜】 |
盘子 | GPRs【通用寄存器集群】 |
锅灶 | ALU |
架子 | 存储器 |
那么计算机是如何工作的呢?
-
做菜前
原材料【数据】和菜谱【指令】按序放在厨房外的架子上【存储器】,且每个架子上有编号【存储单元地址】;
菜谱上的信息有:原料位置、做法、做好的菜放在那里等
例如:将10、11号架子上的原料放在一起炒,并装入3号盘,然后,你告诉妈妈从第5个架子(起始PC=5)指定菜谱开始做
-
开始做菜
- Step 1: 从5号架上取菜谱【根据PC取指令】
- Step 2: 看菜谱【指令译码】
- Step 3: 从架子上或盘子上取原材料【取操作数】
- Step 4: 洗、切、炒等具体操作【指令执行】
- Step 5: 装盘或直接送到桌子上【回写结果】
- Step 6: 算出下一菜谱所在的架子
5+1=6
【修改PC的值】
-
按步继续执行指令【类似存储程序的方式】
1.4.2 计算机工作方式
由妈妈做菜,我们得到计算机的工作方式:
首先,明确程序是由指令构成的【菜单由菜谱构成】
-
程序在执行前
数据和指令事先被放在存储器中,每条指令和每个数据都有地址,指令按序排放,指令由OP和ADDR字段组成,程序起始的位置置PC
原料和菜谱都放在了架子上,每个架子有编号。妈妈从5号架子上拿到制定菜谱开始做菜。
-
开始执行程序
- Step 1: 根据PC取指令【从5号架上拿菜谱】
- Step 2: 指令译码【看菜谱】
- Step 3: 取操作数【从架上或盘上取原材料】
- Step 4: 指令执行【洗、切、炒等具体操作】
- Step 5: 回写结果【装盘或直接送桌】
- Step 6: 修改PC的值【算出下一菜谱所在架子号
5+1=6
】 - While(执行下一条指令)
1.4.3 指令和数据
- 程序启动前:指令和数据都放在存储器中,形式上没有差别,都是
0-1
序列 - 采用存储结构工作方式
- 程序由指令组成,程序被启动后,计算机能自动取出一条又一条的指令执行,而在执行的过程中无需他人干预。
指令中需给出的信息:
- 操作性质【操作码】
- 源操作数1
或/和
源操作数2(立即数、寄存器编号,存储地址)- 单目1个操作数,双目2个操作数
- 目的操作数地址【寄存器编号、存储地址】
- 存储地址的描述与操作数的数据结构有关
1.5 机器语言到高级编程语言
1.5.1 机器语言
用机器语言编写程序【0/1序列】,并记录在纸带或者卡片上
一般为:【举例0010-jc】
0:0101 0110 //前面为操作数 后面为地址数
1:0010 0100
2:......
3:......
4:0110 0111
5:......
6:......
问题:
如果在第4条指令前加入指令,则需要重新计算地址码【如jxx的目标地址】,然后重新打孔。十分不灵活,且阅读、书写困难。
这样太麻烦了,怎么改进咧?
1.5.2 汇编语言
用符号表示跳转位置和变量位置
举个栗子:
add B
jc L0
......
......
L0:sub C
B:......
C:......
可以看到,在第4条指令前加指令时不用改变add
、jxx
和sub
指令中的地址码
简单总结下汇编语言的特点:
- 用助记符来表示操作符:
add
- 用标号表示位置:
B
- 用助记符表示寄存器
- …
汇编语言体现的优点是:
- 不用因为增减指令而去修改其他指令
- 不需要用记忆指令编码,编写方便
- 可读性比机器语言强
带来的新的问题:
- 人容易理解,机器就不一定容易理解了~
因此之后还需要用到汇编程序,将汇编语言转换为机器语言
1.5.3 指令所描述的功能
以“妈妈做菜”为例.
-
Ld M#,R# (将存储单元内容装入寄存器)【Ld == Lord 装入】 St R#,M# (将寄存器内容装入存储单元)【St == Store 存储】 Add R#,M#(类似的还有Sub、Mul等) Jxx M# (若满足条件,转移到另一处执行)
-
汇编语言(源)程序由汇编指令构成
-
一句话描述什么是汇编指令
- 用助记符和标号来表示的指令(与机器指令一一对应)
-
指令又是什么呢
-
包含操作数和操作数或其地址码
机器指令用二进制表示,汇编指令用符号表示
-
只能描述:取(或存一个数);两个数加(或减、乘、除、与、或等);根据运算结果判断是否转移执行
-
-
如果用汇编语言编写复杂程序是什么样的呢?【如实现
sort
或者矩阵相乘】- 需要描述的细节会十分多,程序会十分长,并且在不同结构的机器上不同的汇编编写的程序是不能运行的
因为机器语言和汇编语言都是面向机器结构的语言,故统称它们为机器级语言
1.5.4 用高级语言开发程序
- 高级语言与具体的机器结构无关
- 面向算法描述,比机器级语言描述能力强
- 一条语句对应几条、几十条甚至几百条指令
- 有“面向过程”和“面向对象”语言之分
- 处理逻辑分为三种
- 顺序结构
- 选择结构
- 循环结构
- 两种转换方式:“编译”和“解释”
- **编译程序(Complier):**将高级语言源程序转换为机器级目标程序【.exe可执行文件】,执行时只需要启动目标程序即可
- **解释程序(Interpreter):**将高级语言语句逐条翻译成机器指令并立即执行,不生成目标文件【.py】
1.6 HelloWorld举例
1.6.1 典型程序的转换处理过程
假设"hello.c"程序为:
#include <stdio.h>
int main(){
printf("hello, world\n");
return 0;
}
这个程序的功能为输出hello,world
,但是计算机是不能直接执行hello.c
的,对于hello.c
程序在GCC+Linux
平台中的处理过程为:
hello.c【源程序(文本)】
—预处理(cpp)—>hello.i【源程序(文本)】
—编译(cc1)—>hello.s【汇编语言程序(文本)】
—汇编(as)—>hello.o【可重定位目标程序(二进制)】
—链接(Id)—>hello【可执行目标程序(二进制)】
1.6.2 Hello程序的数据流动过程
先:
shell
命令行处理中:可执行文件加载
后:
hello
程序执行过程
因为数据经常在存储部件间传送,因此现代计算机大多采用缓存的结构
这里有张图
1.6.3 不同层次语言之间的等价转换
高级语言源程序—
编译程序
—>汇编语言源程序–汇编程序
—>机器语言目标程序—指令译码器
—>控制信号
任何高级语言都由若干条指令执行
1.6.4 开发和运行程序
- 最早的程序开发
- 直接输入指令和数据,启动后把第一条指令地址送入
PC
开始执行
- 直接输入指令和数据,启动后把第一条指令地址送入
- 用高级语言开发程序需要复杂的支撑环境
- 需要**
编辑器
**编写源程序 - 需要一套翻译转换软件处理各类源程序
- **编译方式:**预处理程序、编译器、汇编器、链接器
- **解释方式:**解释器等
- 需要一个可以执行程序的界面(环境)【人机接口】
- GUI:图形用户界面
- CUI:命令行用户界面
- 需要**
在指令集体系结构的基础上,用系统软件的语言处理系统和操作系统来开发和运行程序,总结一下为:
- 支撑程序的开发和运行的环境由系统软件提供
- 最重要的系统软件是操作系统和语言处理系统
1.6.4 开发和运行程序
- 最早的程序开发
- 直接输入指令和数据,启动后把第一条指令地址送入
PC
开始执行
- 直接输入指令和数据,启动后把第一条指令地址送入
- 用高级语言开发程序需要复杂的支撑环境
- 需要**
编辑器
**编写源程序 - 需要一套翻译转换软件处理各类源程序
- **编译方式:**预处理程序、编译器、汇编器、链接器
- **解释方式:**解释器等
- 需要一个可以执行程序的界面(环境)【人机接口】
- GUI:图形用户界面
- CUI:命令行用户界面
- 需要**
在指令集体系结构的基础上,用系统软件的语言处理系统和操作系统来开发和运行程序,总结一下为:
- 支撑程序的开发和运行的环境由系统软件提供
- 最重要的系统软件是操作系统和语言处理系统
- 语言处理系统运行在操作系统之上,操作系统利用指令管理硬件