第 3 章 程序的机器级表示
# 第 3 章 程序的机器级表示
# 3.1 历史回顾
# 3.2 程序编码
# 3.2.1 机器级代码
gcc 产生汇编代码
gcc -Og -S mstore.c
-Og
是告诉编译器使用会生成原始 C 代码整体结构的机器代码的优化等级
反汇编器 OBJDUMP
objdump -d mstore.o
# 3.2.2 代码示例
# 3.2.3 关于格式的注解
# 3.3 数据格式
提示
- Intel 用术语 "字(word)" 来表示 16 位数据类型,后缀用
w
来表示 - 32 位被看作 "长字(long word)",因此后缀用
l
表示 - 64 位被称为 "四字 (quad words)",因此后缀用
q
表示
# 3.4 访问信息
x86-64 的整数寄存器
# 3.4.1 操作数指标符
三种操作数类
- 立即数
$
后面跟一个用标准 C 表示法表示的整数
- 寄存器
- 表示某个寄存器的内容
- 内存引用
- 根据计算出来的地址访问某个内存位置
比例变址寻址
- 对应的操作数值为
表示取寄存器值
- 对应的操作数值为
# 3.4.2 数据传送指令
笔记
movl
指令以寄存器作为目的时,会将寄存器的高位 4 字节设置为 0x86-64
采用的惯例,任何为寄存器生成 32 位值的指令都会把该寄存器的高位部分置为 0- 不存在
movzlq
指令,其可以通过movl
指令来实现
- 常规的
movq
只能以表示为 32 位补码数字的立即数作为源操作数,然后将这个值符号扩展到 64 位movabsq
指令能以任意 64 位立即数作为源操作数,但其只能以寄存器作为目的
MOVZ
类零扩展,MOVS
类符号扩展cltq
指令- 没有操作数
- 以寄存器
%eax
作为源,%rax
作为符号扩展结果的目标寄存器
# 3.4.3 数据传送示例
笔记
- C 中指针的间接引用
*p
既可以当左值,又可以当右值- 与汇编中内存地址,如
(%rdi)
,即能当mov
指令的Source
,又能当mov
指令的Dest
相对应 - 当
Source
时,即为右值 - 当
Dest
时,即为左值
- 与汇编中内存地址,如
- 局部变量通常保存在寄存器中,而不是内存中
- 访问寄存器比访问内存要快很多
# 3.4.4 压入和弹出栈数据
提示
%rsp
保存着栈顶元素的地址- 栈是向下增长的
# 3.5 算术和逻辑操作
整数算术操作
# 3.5.1 加载有效地址
lea(load effective address): 加载有效地址
- 并不从指定的位置读入数据,而是将有效地址写入到目的操作数
- 也可以简洁的描述普通的算术操作
- 目的操作数必须是一个寄存器
# 3.5.2 一元操作和二元操作
笔记
- 一元操作只有一个操作数,既是源又是目的
- 二元操作中,第二个操作数即是源又是目的
# 3.5.3 移位操作
笔记
- 移位量要么是一个立即数,要么放在单字节寄存器
%cl
中 - x86-64 中,对
w
位长的数据进行移位操作时- 移位量由
%cl
寄存器的低m
位决定,,高位会被忽略
- 移位量由
- SAL 和 SHL
- 算术左移和逻辑左移,两者效果相同
- SAR 和 SHR
- 算术右移和逻辑右移
- 算术右移会填上符号位
# 3.5.4 讨论
提示
- 整数的算术操作符中,只有算术右移需要区分有符号数和无符号数
- 其它指令既可用于无符号运算,也可用于补码运算
# 3.5.5 特殊的算术操作
笔记
- 16 字节数(128 位)称为八字(oct word),简写为
o
imulq
根据操作数的数量,有两种不同的乘法指令,分别对应 64 位的乘法和 128 位的乘法- 两个操作数,则对应于 64 位的乘法
- 从两个 64 位的操作数产生 64 位的乘法
- 一个操作数,则对应于 128 位乘法
- 另一个参数必须位于寄存器
%rax
中 - 乘积的高 64 位存放在寄存器
%rdx
中,低 64 位存放在寄存器%rax
中
- 另一个参数必须位于寄存器
- 两个操作数,则对应于 64 位的乘法
- 有符号除法指令
idivl
- 将寄存器中
%rdx
(高 64 位)和%rax
(低 64 位) 中的 128 位作为被除数 - 除数作为指令的操作数给出
- 商储存在寄存器
%rax
中,而余数储存在寄存器%rdx
中
- 将寄存器中
cqto
指令- 读出
%rax
的符号位,并将其复制到%rdx
的所有位
- 读出
图示
# 3.6 控制
提示
- 机器代码提供两种基本的低级机制来实现有条件的行为
- 测试数据值
- 根据测试的结果来改变控制流或数据流
# 3.6.1 条件码
条件码寄存器
CF
进位标志- 可以用来检查无符号操作的溢出
ZF
零标志SF
符号标志OF
溢出标志- 补码溢出
提示
leaq
指令不改变任何条件码,其只被用来进行地址计算- 逻辑操作,进位标志和溢出标志均被设为0
- 移位操作,进位标志将设置为最后一个被移出的位,而溢出标志被置为 0
- INC 和 DEC 会设置溢出和零标志,但是不会改变进位标志
CMP 指令和 TEST 指令
- 只设置条件码,不改变其它任何寄存器
CMP S1, S2
- 根据
S2 - S1
的值来设置条件码
- 根据
TEST S1, S2
- 根据
S1 & S2
的值来设置条件码 - 与
AND
指令的行为一样,但是其不会改变寄存器的值 testq %rax, %rax
用来测试%rax
的符号
- 根据
# 3.6.2 访问条件码
SET
指令
- 根据条件码的某种组合,将一个字节设置为 0 或 1
所有的 SET
指令
g
和l
对应于有符号数a
和b
对应于无符号数
# 3.6.3 跳转指令
所有跳转指令
- 间接跳转中的
*
相当于 C 语言与指针配对的间接访问操作符
# 3.6.4 跳转指令的编码
笔记
- 一个字节表示为跳转指令,其它字节作为偏移地址
- 偏移地址计算时,采取的基址是跳转指令下一条指令的地址
# 3.6.5 用条件控制来实现条件分支
提示
%rip
寄存器与全局变量相关rep
与repz
指令相当于空操作- 具体作用参照 P141
C 语言中的 if-else
语句
if (test-expr)
then-statement
else
else-statement
2
3
4
if-else
语句的汇编实现
t = test-expr;
if (!t)
goto false;
then-statement
goto done;
false:
else-statement
done:
2
3
4
5
6
7
8
- 如果没有
else
语句,则可以将false
标签合并到done
标签
# 3.6.6 用条件传送来实现条件分支
笔记
- 控制 的条件转移在现代处理器上,可能非常低效
- 替代策略是 数据 的条件转移
- 计算一个条件操作的两种结果,然后再根据条件是否满足从中选取一个
- 可以使用 条件传送 指令来实现
- 条件传送指令更加符合现代处理器的性能特征
条件传送指令
提示
- 使用条件传送指令与流水线相关
- 条件传送指令使得控制流不依赖于数据,使得处理器更容易保持流水线是满的
- 使用条件传送指令的前提条件是分支没有副作用
- 需要综合考虑使用条件分支浪费的计算和由于分支预测错误所造成的性能处罚之间的相对性能
# 3.6.7 循环
do-while
循环
通用形式
do body-statement while (test-expr);
1
2
3翻译成
goto
语句loop: body-statement t = test-expr; if (t) goto loop;
1
2
3
4
5
while
循环
通用形式
while (test-expr) body-statement
1
2跳转到中间 (jump to middle) 翻译方法
goto test; loop: body-statement test: t = test-expr; if (t) goto loop;
1
2
3
4
5
6
7guarded-do 翻译方法
翻译为
do-while
循环的形式t = test-expr; if (!t) goto done; do body-statement while (test-expr); done:
1
2
3
4
5
6
7goto
代码如下t = test-expr; if (!t) goto done; loop: body-statement t = test-expr; if (t) goto loop; done:
1
2
3
4
5
6
7
8
9使用较高优化等级编译时,如
gcc
使用选项-O1
,会采用这种策略- 利用这种实现策略,编译器常常可以优化循环内的初始测试,参照 图 3-21(P154)
for
循环
通用形式
for (init-expr; test-expr; update-expr) body-statement
1
2基本等价于如下
while
循环init-expr; while (test-expr) { body-statement update-expr; }
1
2
3
4
5continue
语句是个例外- 此时需用
goto
替代continue
,直接跳至update
部分 - 参照练习 3-29 (P159)
- 此时需用
# 3.6.8 switch
语句
笔记
switch
语句可以根据一个整数索引值进行多重分支switch
可以提高 C 代码的可读性- 通过使用 跳转表 (jump table)这种数据结构可以让实现更加高效
跳转表
- 使用跳转表的优点是执行开关语句的时间与开关情况的数量无关
- GCC 会根据开关情况的数量和开关情况值的稀疏程度来翻译开关语句
- 当开关情况数量比较多,并且值的范围跨度比较小时,会使用开关表
- GCC 中有一个新的运算符
&&
,这个运算符创建一个指向代码位置的指针
# 3.7 过程
假设过程 P
调用过程 Q
,Q
执行后返回到 P
设计良好的软件用过程作为抽象机制
- 隐藏某个行为的具体实现
- 提供清晰简洁的接口定义
- 说明要计算的是哪些值
- 过程会对程序状态产生怎样的影响
要提供对过程的机器级支持,需要处理许多不同的属性
- 传递控制
- 进入
Q
时,程序计数器必须设置为Q
的代码的起始地址 - 在返回时,要把程序计数器设置为
P
中调用Q
后面那条指令的地址
- 进入
- 传递数据
P
必须能向Q
提供一个或多个参数Q
必须能向P
返回一个值
- 分配和释放内存
- 在开始时,
Q
可能需要为局部变量分配空间 - 在返回前,必须释放这些存储空间
- 在开始时,
X86-64 的过程实现包括一组特殊的指令和一些对于机器资源使用的约定规则
- 特殊的指令包括
call(callq)
和ret(retq)
- 机器资源包括寄存器和程序内存
- 约定的规则即为 ABI(二进制程序接口)
目的是为了尽量的减少过程调用的开销
# 3.7.1 运行时栈
栈数据结构提供的后进先出的内存管理规则与过程的调用-返回顺序匹配
- 将栈指针
%rsp
减小一个适当的量可以为没有指定初始值的数据在栈上分配空间 - 同样,可以增加栈指针来释放空间
过程的栈帧(stack frame)
- 当 x86-64 过程需要的储存空间超出寄存器能存放的大小时,就会在栈上分配空间
通用的栈帧结构
- 许多函数不需要栈帧
- 当所有的局部变量都可以保存在寄存器中,而且该函数不会调用任何其他函数时,则不需要栈帧
- 这种过程被称为叶子过程,可以将过程调用看做树结构
- 当所有的局部变量都可以保存在寄存器中,而且该函数不会调用任何其他函数时,则不需要栈帧
# 3.7.2 转移控制
call
和 ret
call Q
会将地址A
压入栈中,并将 PC 设置为Q
的起始地址- 地址
A
为返回地址,是紧跟在call
指令后的那条指令的地址- 属于
P
的栈帧
- 属于
- 地址
- 栈指令
ret
会从栈中弹出地址A
,并将 PC 设置为A
提示
callq
的retq
后的q
是为了强调是 x86-64 版本的调用和返回- PC(程序寄存器) 的值存储在寄存器
%rip
中 - 把返回地址压入栈的简单的机制能够让函数 在稍后返回到程序中正确的点
# 3.7.3 数据传送
通过寄存器传递函数参数
- 通过寄存器最多能传递 6 个整形(即整数和指针)参数
传递整型函数参数的寄存器
- 整型参数超出 6 个的部分需要通过栈来传递
- 参数 7~n 放置在栈上,而参数 7 位于栈顶
- 通过栈传递参数时,所有的数据大小都向 8 的倍数对齐
参数
a4
即为对齐后的结果
- 通过栈传递参数时,所有的数据大小都向 8 的倍数对齐
- 参数 7~n 放置在栈上,而参数 7 位于栈顶
# 3.7.4 栈上的局部存储
有些时候,局部数据必须存放在内存中
- 寄存器不足够存放所有的本地数据
- 对一个局部变量使用地址运算符
&
- 必须为它产生一个地址
- 某些局部变量是数组或结构
- 必须能够通过数组或结构引用被访问到
运行时栈提供了一种简单的、在需要时分配、函数完成时释放的局部存储的机制
# 3.7.5 寄存器中的局部存储空间
笔记
- 寄存器是唯一被所有过程共享的资源
- 虽然在给定的时刻只有一个过程是活动的,但必须确保当一个过程调用另一个过程时
- 被调用者不会覆盖调用者稍后会使用的寄存器值
- 为此, x86-64 采用了一组统一的寄存器使用惯例,所有的过程都必须遵守
寄存器使用惯例
- 被调用者保存寄存器
%rbx, %rbp
和%r12 ~ %r15
- 调用者保存寄存器
- 所有其他的寄存器,除了栈指针
%rsp
,都被分类为调用者保存寄存器
- 所有其他的寄存器,除了栈指针
如果没有使用,则不需要保存
# 3.7.6 递归过程
寄存器和栈的使用惯例使得 x86-64 过程能够递归的调用其自身
# 3.8 数组的分配和访问
提示
- 参照 C 和指针 -- 数组
笔记
- C 语言可以产生指向数组中元素的指针,并对这些指针进行运算
- 优化编译器非常善于简化数组索引所使用的地址计算
- 如尽量减少乘法的使用
# 3.8.1 基本原则
提示
- x86-64 的内存引用指令可以简化数组访问
- 伸缩因子
1, 2, 4, 8
覆盖了所有基本简单数据类型的大小
- 伸缩因子
# 3.8.2 指针运算
提示
- 参照 指针运算
# 3.8.3 嵌套的数组
提示
- 参照 多维数组
- 数组元素在内存中按照 "行优先" 的顺序排列
# 3.8.4 定长数组
提示
- 编译器会借助已知的信息对数组的下标索引进行优化
- 参照 P180 页的示例
# 3.8.5 变长数组
提示
C99 允许数组的维度是表达式,在数组被分配的时候才计算出来
一般用于函数中
int var_ele(long n, int A[n][n], long i, long j){ return A[i][j]; }
1
2
3- 参数
n
必须出现在A[n][n]
之前
- 参数
在一个循环中引用变长数组时,编译器常利用访问模式的规律性来优化索引的计算
- GCC 能识别出程序访问多维数组元素的元素的步长,然后生成代码来避免使用乘法
# 3.9 异质的数据结构
提示
- 参照 C 和指针 -- 结构和联合
- 异质的数据结构指将不同类型的对象组合到一起创建数据类型
# 3.9.1 结构
结构的特点
- 结构的所有组成部分都放在内存中的一段连续的区域内
- 指向结构的指针就是结构第一个字节的地址
- 编译器维护关于每个结构类型的信息,指示每个字段 (field) 的字节偏移
- 以这些偏移作为内存引用指令中的位移,从而产生对结构元素的引用
- 结构各个字段的选取完全是在编译器处理的,机器代码不包含关于字段声明或字段名字的信息
# 3.9.2 联合
联合的特点
联合用不同的字段引用相同的内存块
一个联合总的大小等于它最大字段的大小
当结构中两个字段的使用是互斥的时候,使用联合可以减小分配空间的问题
联合通常的使用方法是引入一个枚举类型,定义这个联合中可能的不同选择,然后再创建一个结构,包含一个标签字段和这个联合
示例:二叉树结点,内部节点不含数据,叶子节点含有两个
double
类型的数据typedef enum { N_LEAF, N_INTERNAL } nodetype_t; struct node_t { nodetype_t type; union { struct { struct node_t *left; struct node_t *right; } internal; double data[2]; } info; };
1
2
3
4
5
6
7
8
9
10
11
联合可以用来访问不同数据类型的位模式
当用联合将各种不同大小的数据类型结合到一起时,需要注意字节顺序的问题
# 3.9.3 数据对齐
笔记
许多计算机系统对基本数据类型的合法地址做了一些限制,要求某种类型对象的地址必须是某个值
的(通常是 2, 4, 8) 倍数 - 处理器一般一次从内存中取出多个字节
- 如果没有按要求对齐,则为了读取一个基本数据类型,可能需要执行两次内存访问
- 数据对齐可以提升内存系统的性能
x86-64 硬件的对齐原则是作为任何
字节的基本对象的地址必须是 的倍数 汇编代码
.align 8
1- 保证了它后面的数据的起始地址是 8 的倍数
结构中的基本数据结构都要满足对齐的要求
为了保证结构数组中的每个元素都满足它的对齐要求,结构的末尾可能需要一些填充
- 保证结构的首字节是满足对齐要求的
当结构中所有数据元素的长度都是 2 的幂时
- 一种行之有效的策略是按照大小的降序排列结构中的元素
- 以减少空间的占用
# 3.10 在机器级程序中将控制与数据结合起来
# 3.10.1 理解指针
笔记
- 每个指针都对应一个类型
- 指针类型不是机器代码的一部分,只是 C 语言提供的一个抽象
- 每个指针都有一个值
- 这个值是某个指定类型的对象的地址
NULL(0)
值表示指针没有指向任何地方
- 指针用
&
运算符创建- 此运算符的机器代码实现常用
lea
指令实现
- 此运算符的机器代码实现常用
*
操作符用于间接引用指针- 通过内存引用来实现
- 数组与指针紧密联系
- 一个数组名可以像一个指针变量一样使用(但不能被修改)
- 指针的强制类型转换,只改变类型,不改变值
- 指针也可以指向函数
int (*f)(int *)
f
是指向以int *
为参数,并返回int
的函数的指针
int *f(int *)
- 会被解释为一个函数原型
# 3.10.2 使用 GDB 调试器
常用的 GDB 命令
# 3.10.3 内存越界引用和缓冲区溢出
内存越界引用发生的原因
- C 对数组引不进行任何边界检查
- 局部变量和状态信息(如保存的寄存器值和返回地址)都存放在栈中
后果
- 对越界的数组元素的写操作会破坏存储在栈中的状态信息
- 当程序试图重新加载寄存器或执行
ret
指令时,就会发生严重的错误
缓冲区溢出
- 可以利用缓冲区溢出让程序执行攻击者注入的代码
- 任何到外部环境的接口都应该是“防弹的”
- 能够避免外部代理行为导致的系统错误
蠕虫与病毒
- 蠕虫可以自己运行,并能够将自己的等效副本传播到其他机器
- 病毒能将自己添加到包括操作系统在内的其它程序中,但它不能独立运行
# 3.10.4 对抗缓冲区溢出攻击
栈随机化
- 使栈的位置在每次运行时都有变化
- 在程序开始时,在栈上分配一段
0~n
字节的随机大小的空间
- 在程序开始时,在栈上分配一段
- 在 Linux 系统中,栈随机化已成为标准化行为
- 属于地址空间布局随机化(Address-Space Layout Randomization, ASLR)
栈破坏检测
- 在栈帧的任何局部缓冲与栈状态之间存储一个特殊的金丝雀(canary)值,也叫做哨兵值
示意图
- 程序每次运行时随机产生
- 在恢复寄存器或从函数返回时,检查这个金丝雀值是否被修改
- 如果被修改,则程序异常中止
- 只会带来很少的性能损失,gcc 只在函数中有局部
char
类型缓冲区的时候才会插入这样的代码- gcc 的命令行选项
-fno-stack-protector
可以阻止生成相应的保护代码
- gcc 的命令行选项
限制可执行代码区域
- 目的是消除攻击者向系统中插入可执行代码的能力
常用的对抗缓冲区溢出攻击的方法不需要程序员做出特殊的努力,带来的性能代价也非常小
# 3.10.5 支持可变长栈帧
使用情形
alloca
是一个标准库函数,可以在栈上分配任意字节数量的存储- 当函数声明一个局变可变数组时,也要求栈帧的长度可变
x86-64 使用寄存器 `%rbp` 作为帧指针(frame pointer) 来管理可变长栈帧
%rbp
也称为基指针(base pointer)使用示意图
leave
指令相当于以下两条指令
movq %rbp, %rsp ;Set stack pointer to beginning of frame popq %rbp ;Restore saved %rbp and set stack ptr to end of caller’s frame
1
2用于在函数返回时,将帧指针恢复到之间有状态
# 3.11 浮点代码
笔记
- 单指令多数据 或 SIMD
- 媒体指令,支持图形和图像处理
- 允许对不同的数据并行执行同一操作
- 最新的为 AVX(Advanced Vector Extension,高级向量扩展)
- 给定命令行参数
-mavx2
,gcc 会生成 AVX2 代码
寄存器
- AVX 浮点体系结构允许数据存储在 16 个 YMM 寄存器中,名字为
%ymm0 ~ %ymm15
- 每个都是 256 位(32 字节)
- 当对标题(scalar)数据进行操作时,这些寄存器只保存浮点数
- 对于
float
,只使用低 32 位 - 对于
double
,只使用低 64 位 - 汇编代码用
%xmm0 ~ %xmm15
来引用他们
- 对于
AVX 浮点体系媒体寄存器
# 3.11.1 浮点传送和转换操作
浮点传送指令
- 引用内存的指令是 标量 指令
- 只对单个而不是一组封装好的数据值进行操作
- 浮点传送指令建议 32 位内存数据满足 4 字节对齐,64 位数据满足 8 字节对齐
浮点传递指令图示
浮点转换
- 双操作数浮点转换指令
命令说明
- 将浮点数转换为整数
- 会进行截断(truncation),把值向 0 进行舍入
- 三操作数浮点转换指令
命令说明
- 可以忽略第二个操作数,因为其只影响高位字节
- 在常见的使用场景中,第二个源和目的操作数是一样的
float
和 double
之间的转换
float
todouble
;Conversion from single to double precision vunpcklps %xmm0, %xmm0, %xmm0 ;Replicate first vector element vcvtps2pd %xmm0, %xmm0 ;Convert two vector elements to double
1
2
3double
tofloat
;Conversion from double to single precision vmovddup %xmm0, %xmm0 ;Replicate first vector element vcvtpd2psx %xmm0, %xmm0 ;Convert two vector elements to single
1
2
3
# 3.11.2 过程中的浮点代码
寄存器使用规则
- XMM 寄存器
%xmm0 ~ %xmm7
最多可以传递 8 个浮点参数- 按照参数列出的顺序使用这些寄存器
- 多余的参数通过栈进行传递
- 函数使用寄存器
%xmm0
来返回浮点值 - 所有的 XMM 寄存器都是调用者保存的
当函数包含指针、整数和浮点数混合的参数时
- 指针和整数通过通用寄存器传递
- 浮点值通过 XMM 寄存器传递
# 3.11.3 浮点运算操作
标题浮点算术运算操作指令
图示
- 每条指令有一个(
)或两个( )源操作数和一个目的操作数 和 都必须是 XMM 寄存器
# 3.11.4 定义和使用浮点常数
笔记
- AVX 浮点操作不能以立即数值作为操作数
- 编译器必须为所有的常量值分配和初始化存储空间
示例
- C code
double cel2fahr(double temp) { return 1.8 * temp + 32.0; }
1
2
3
4- 汇编代码
;double cel2fahr(double temp) ;temp in %xmm0 cel2fahr: vmulsd .LC2(%rip), %xmm0, %xmm0 ;Multiply by 1.8 vaddsd .LC3(%rip), %xmm0, %xmm0 ;Add 32.0 ret .LC2: .long 3435973837 ;Low-order 4 bytes of 1.8 .long 1073532108 ;High-order 4 bytes of 1.8 .LC3: .long 0 ;Low-order 4 bytes of 32.0 .long 1077936128 ;High-order 4 bytes of 32.0
1
2
3
4
5
6
7
8
9
10
11
12- 注意常量是以十进制的形式书写的,而且由于小端字节序,低位字节在前,高位字节在后
# 3.11.5 在浮点代码中使用位级操作
位级操作指令
- 注意会更新整个目的 XMM 寄存器
# 3.11.6 浮点比较操作
浮点比较指令
指令详情
- 参数
必须在 XMM 寄存器中
条件码
- 浮点比较指令会设置如下 3 个条件码
ZF
CF
PF
- 为奇偶标志位
- 对于整数操作,当最近一次运算产生的值的最低位字节是偶校验的,则会设置这个标志位
- 偶校验是指字节中有偶数个 1
- 对于浮点比较,则当两个操作数中的任一个是
NaN
时,就会设置该位 jp(jump on parity)
是针对此标志位的条件跳转
条件码的设置条件
- 注意当任一操作数为
NaN
时,jb
、je
和jbe
都会发生跳转,参照 跳转指令- 因为无序时其会设置
CF
和ZF
为1
- 因为无序时其会设置
- 注意当任一操作数为
# 3.11.7 对浮点代码的观察结论
提示
- AVX2 有能力在封装好的数据上执行并行操作,便计算执行得更快
- 从标量代码到并行代码的转换可以使用 GCC 支持的、操纵向量数据的 C 语言扩展,参照 P376 页