80x86汇编与C语言-4

官方信息技术老师·12 页·深入(追求细节与边界)·0 次浏览·3 天前
编译原理调用约定栈帧布局汇编映射

80x86汇编与C语言-4

看清C代码每行对应的汇编指令,掌握函数调用与栈帧布局

按 空格/→ 演示下一步

1 / 12 页

全部页面点击任意一页,跳回舞台从这页播放

编译原理调用约定栈帧布局汇编映射

80x86汇编与C语言-4

看清C代码每行对应的汇编指令,掌握函数调用与栈帧布局

1第 1 页 · 80x86汇编与C语言-4

数组的存储

在C里写 int a[5],用 a[i] 取值很简洁。但CPU不懂「数组」——它只认内存地址。这一页拆开看:编译器把你的数组翻成什么内存布局,CPU又怎么算出每个元素的地址。

连续存储
元素在内存中紧密排列,首尾相接,中间无缝隙
基地址+偏移
元素地址 = 数组首地址 + 下标 × 单元素字节数
数组名即首址
a 在表达式中等价于 &a[0],是 T* 类型常量
80x86寻址方式
[bx+si] 或 386 的 [base+index*scale],硬件自动按类型宽度乘 scale
类型决定步长
char=1B,short=2B,int 在16/32位模式下分别为 2B/4B
酒店连排房间对应 →数组的内存布局

房间等大且相邻,房号=起始房号+序号×单房宽度,楼层对应基地址

2第 2 页 · 数组的存储

数组的访问-1

上一页我们看到数组在内存里排成一长串——但 CPU 执行 a[3] 时,它不会自动跳到第三个位置。它只认地址,必须把下标翻译成具体地址才能取到数。

地址 = 基址 + 偏移
数组首地址是基址,下标乘元素大小得到偏移量,相加即得目标地址
元素大小参与乘法
int 数组每元素 4 字节,偏移 = i × 4;char 数组每元素 1 字节,偏移 = i
汇编的寻址公式
[base+index*scale+disp],scale 取 1/2/4/8,由元素大小决定
a[i] 本质是指针算术
C 中 a[i] 与 *(a+i) 完全等价,加法由编译器按 sizeof 缩放后翻译成寻址
电影院找座位对应 →数组寻址

影院地址=基址,排号=下标,排距=元素大小,座位左右偏移=disp

addr=base+index×sizeof(T)+offsetaddr = base + index \times \text{sizeof}(T) + \text{offset}
3第 3 页 · 数组的访问-1

数组的访问-2

上页算出元素地址=基础地址+偏移,但C里写a[i]就拿到值——背后是指针算术。指针+1不是加一字节,而是跨过一个元素,汇编用一条寻址指令就能完成。

指针算术
p+i 自动按 sizeof(*p) 步进,不是按字节加 1
SIB 寻址
[base+index*scale+disp] 把乘加融成一条指令
scale 字段
取值 1/2/4/8,对应常见元素字节数
越界无检查
C 与汇编都不会阻止指针走出数组范围
电影院找座位对应 →数组指针算术

第5座=第1座起点+4×每座宽度,scale 就是每座宽度

addr(a[i])=base+i×sizeof(elem)\text{addr}(a[i]) = \text{base} + i \times \text{sizeof}(\text{elem})
4第 4 页 · 数组的访问-2

数组的访问-3

前两页我们看到 a[i] 就是「首地址 + 偏移」。但遇到二维数组 a[i][j],CPU 怎么一步算出地址?这就涉及到行主序约定和地址公式。

行主序存储
C 语言规定:同一行的元素在内存中连续存放,下一行紧跟其后
地址计算公式
a[i][j] 的地址 = 基地址 + (i × 列数 + j) × 元素大小
汇编寻址模式
80x86 用 [base + index × scale + disp] 一条指令完成地址计算
典型应用
图像像素矩阵、矩阵乘法、棋盘格数据都依赖该公式
电影院找座位对应 →二维数组寻址

先确定第几排(i × 每排座位数),再数第几个座位(j),偏移量自然算出

addr(a[i][j])=base+(i×N+j)×sizeof(T)\text{addr}(a[i][j]) = \text{base} + (i \times N + j) \times \text{sizeof}(T)
5第 5 页 · 数组的访问-3

数组的访问-4

前几页默认每个元素 1 字节。但 short 是 2、int 是 4、long 是 8——汇编怎么用同一个公式访问任意大小?这就靠 80x386 引入的「比例因子」。

元素大小 = 步长
byte 数组下标乘 1,word 数组乘 2,dword 数组乘 4
比例因子 scale
寻址格式 [基址 + 索引×scale] 中,scale 只能取 1/2/4/8
偏移公式
偏移 = 基址 + 下标 × sizeof(元素) + 常量偏址
C 自动 vs 汇编手动
C 编译器按 sizeof 替你乘;汇编必须写 scale 或手动 SHL
书架上每格书的厚度不同对应 →不同类型数组的访问步长

想拿第 N 格的书,得先知道单本多厚,位置 = 起点 + N × 厚度

addr=base+index×sizeof(T)+disp\text{addr} = \text{base} + \text{index} \times \text{sizeof}(T) + \text{disp}
6第 6 页 · 数组的访问-4

数组的访问-5

前面看到C的arr[i]会被编译成一条mov指令。但80x86具体用哪种寻址来编码?这关系到代码大小和执行速度——今天看SIB字节和比例因子是怎么被'精打细算'选出来的。

SIB寻址格式
[base+index*scale+disp],base是数组首址,index是下标,scale按元素宽度
scale自动选取
编译器按元素宽度选1/2/4/8,把乘法直接交给硬件省掉
disp长度优化
能用disp8就不用disp32,指令短一字节,I-cache更友好
段前缀按需
默认走DS;想访问ES/FS等段要显式加段前缀字节
电影院找座位对应 →SIB寻址

厅号+排号×每排数+座号,三项相加锁定一个具体观众

[base+index×scale+disp][\text{base} + \text{index} \times \text{scale} + \text{disp}]
7第 7 页 · 数组的访问-5

二维数组示例-1

上几页我们把一维数组摸透了——一段连续内存加下标算偏移。那二维数组呢?比如一张成绩表、一个图像像素矩阵,内存里它是怎么摆的?

行优先存储
二维数组在内存中按行展开成一段连续空间,行与行紧挨着
元素地址公式
a[i][j] 地址 = base + (i × 列数 + j) × 元素字节大小
C 双重下标
arr[i][j] 被翻译为 *(*(arr+i)+j),本质是两次解引用
汇编手动算偏移
需 imul 算 i×列数,加 j,再按 1/2/4 字节换算成字节偏移
类型决定列宽
int a[3][4] 与 int **p 不同,前者编译期就知道列宽
电影院一排排座位对应 →二维数组行优先存储

座位号连续:1排1号…1排N号,接着才是2排1号…数组也是这样展开

addr(a[i][j])=base+(i×N+j)×sizeof(T)\text{addr}(a[i][j]) = \text{base} + (i \times N + j) \times \text{sizeof}(T)
8第 8 页 · 二维数组示例-1

二维数组示例-2

上一页用二维表理解 a[i][j],但内存是线性的,C 按行连续排列,这决定了所有寻址方式。

行优先存储
C 按行号递增连续排列各行元素,同行相邻元素地址相差一个元素大小
地址公式
a[i][j] 的地址 = base + (i × 列数 + j) × sizeof(元素类型)
指针等价
a[i][j] 等价于 *(*(a+i)+j),拆解为两步指针解引用
数组名类型
a 退化为「指向含 cols 个元素的数组」的指针,类型为 int(*)[N]
电影院找座位对应 →二维数组寻址

先按楼层找排再按位置找座:地址 = 楼号×每排座位数 + 座号

&a[i][j]=&a[0][0]+(i×N+j)×sizeof(T)\&a[i][j] = \&a[0][0] + (i \times N + j) \times \text{sizeof}(T)
9第 9 页 · 二维数组示例-2

二维数组示例-3

示例-2 揭示了二维数组在内存里是连续排列的。那 C 写 a[i][j] 时,编译器到底怎么把这两个下标翻译成线性地址?

地址公式
&a[i][j] = base + (i × 列数 + j) × 元素大小
行优先约定
C 标准规定按行连续存储,先放满第 0 行再放第 1 行
汇编实现
lea + 乘法算偏移;编译器常优化为 i×行字节数 + j×元素大小
电影院凭票找座对应 →二维数组按下标寻址

先跳几整排(i × 列数),再从该排头数 j 个座位

addr(a[i][j])=base+(i×N+j)×saddr(a[i][j]) = base + (i \times N + j) \times s
10第 10 页 · 二维数组示例-3

本节要点

  • 数组在内存中是连续等长的数据块
  • 元素地址=基址+索引×元素宽度,汇编需按宽度缩放
  • 二维数组按行主序连续存放,可展平为一维看待
  • C不做越界检查,汇编必须由程序员核对索引范围
  • 循环遍历时指针自增比反复计算下标更高效
延伸主题:结构体的内存对齐函数调用栈帧分析指针算术与类型系统
11第 11 页 · 本节要点

课后思考

先独立思考再对照参考答案。三个问题分别检验核心理解、汇编应用与越界边界。

1为什么C语言中数组下标从0开始,而不是从1开始?

参考答案a[i] 等价于 *(a + i)。从0开始时偏移量就是 i*sizeof(元素);若从1开始则要先减1,多一条运算指令。0起点还让首元素地址与数组名一致,惯例沿用至今。

2对同一个二维数组,按行遍历与按列遍历,编译器生成的汇编会有何不同?为什么?

参考答案按行遍历时内层循环只调整列偏移,访存地址连续;按列遍历每次跳一整行长度。汇编上对应不同的基址更新方式,连续访问更利于CPU预取与缓存命中。

3C语言不检查数组越界,这在汇编层面意味着什么?可能引发哪些问题?

参考答案汇编里没有'数组'概念,只有基址加偏移寻址。越界只是算出一个错误地址,可能踩到栈上返回地址、相邻变量或函数指针,经典栈溢出攻击即源于此。

12第 12 页 · 课后思考