深入理解计算机系统(笔记):程序的机器级表示


分析高级语言编译后生成的汇编语言。


1. 程序编码

运行如下命令得到C语言的汇编代码:

unix> gcc -O1 -S code.c


gcc -c选项编译源文件生产目标文件code.o:

unix> gcc -O1 -c code.c


可以使用反汇编器(disassembler)来查看目标文件内容:readelf/objdump

unix> objdump -d code.o


gcc -o选项可连接不同的目标文件,生产可执行文件:

gcc -O1 -o prog code.o main.c


可执行文件prog比单纯的目标文件大了许多,因为它不仅包含了源文件的代码,还包含了与操作系统交互的程序(用于启动和终止程序等)。objdump也可用于分析可执行文件:

unix> objdump -d prog


2. 数据格式


大多数GCC生成的汇编代码都有一个字符后缀,表明操作数大小。如mov指令有3个变种:movb(字节)、movw(字)、movl(双子)。


3. 访问信息

CPU(IA32)包含8个32bits的寄存器,寄存器以%开头,可表示8位(%al, %ah)、16位(%ax)、32位(%eax)数据



3.1 操作数指示符

多少汇编指令都有操作数来指定操作中使用的数据,以及结果存放的位置。IA32支持多种操作数格式


Imm一般表示立即数或者偏移量,Eb表示基址寄存器,Ei表示变址寄存器,s为比例因子。

立即数和寄存器很好理解,第三个绝对寻址就是用常数(或常数表达式)直接给出操作数在内存中的地址,第四个间接寻址就是将内存地址放在寄存器里,第五个是基址(寄存器的值,基址寄存器)加上偏移量(Imm的值)形成的地址,第六个就基址(基址寄存器)加上变址(变址寄存器)形成的地址,第七个是前一个的基础上加一个偏移,剩下的比例变址寻址就是前面的值相加再加上变址寄存器与比例因子的乘积(Ei * s)。

复杂的寻址方式比较适合于数组和结构元素。


3.2 数据传送指令

mov和push、pop都可以传送数据


mov系列指令有两个操作数,IA32限制他们不能都为内存地址,但是都可以使寄存器。

MOVS和MOVZ都是将一个较小的源数据复制到一个较大的数据位置,高位(就是目的数据多出的bit位)用符号位(MOVS)或者零(MOVZ)填充。

3种mov指令的例子如下:



3.3 数据传送示例



4. 算术和逻辑操作

下图列出了一些整数和逻辑操作,除leal外,其他指令都有根据操作数大小而不同的变种,如ADD有3种:addb(字节)、addw(字)、addl(双字)。这些指令分为四类:加载有效地址、一元操作、二元操作和移位

指令 leal 将有效地址写入目的操作数(只能为寄存器),如果寄存器%edx的值为x,那么指令 leal 7(%edx,%edx,4),%eax 将寄存器%eax的值设置为 5x + 7,而movl指令会将内存中 5x + 7 位置上的数据送入目的操作数。是不是有点像指针?

二元指令中操作数不能都是内存位置。


下图是一些特殊的算术操作



5. 控制

条件控制、循环控制、分支控制。


5.1 条件码

CPU有一个条件码寄存器,记录一些标志位,常用标志位:

CF:进位标志,carry

ZF:零标志,zero

SF:符号标志,sign

OF:溢出标志,overflow


运算指令(除leal外)都会影响条件寄存器。

CMP与TEST指令进行实际运算,只改变条件码。CMP与SUB一样对两个操作数执行减法,根据结果设置相应标志位;TEST与ADD一样对两个操作数执行加法,根据结果设置相应标志位。他们也分别有3种变种:b、w、l


5.2 访问条件码

三种常用方法:根据条件码设置一个数据为0或者1;根据条件码跳转到其他部分;有条件地传送数据。

使用SET指令,可以根据条件码设置一个字节为0或1


5.3 跳转指令及其编码

JMP 系列指令跳转到指定位置执行代码,或者根据条件码决定要不要跳转到新的代码位置。


5.4. 翻译条件分支

图a为计算两数之差的C代码,图b为对应的使用goto语句的C代码,图c为GCC生成的汇编代码


C语言中的if-else语句形式如下:

if (test-expr)
    then-statement
else
    else-statement
对应的汇编形式如下:

t = test-expr;
if (!t)
    goto false;
then-statement;
goto done;

false:
    else-statement;
done:

5.5 循环

C语言中的循环:do-while、while、for,汇编指令动态条件测试和跳转指令来实现。

5.5.1 do-while

do

    body-statement

while (test-expr)

翻译成如下goto形式

loop:

    body-statement

    t = test-expr;

    if (t)

        goto loop;



5.5.2 while循环

while语句通用形式:

while (test-expr)

    body-statement

先转换成do-while形式:

if (!test-expr)

    goto done;

do

    body-statement

    while (test-expr)

done:

再转换成do-while对应的goto形式:

t = test-expr;

if (!it)

    goto done;

loop:

    body-statement

    t = test-expr;

    if (t)

        goto loop;

done:

实例如下:



5.5.3 for循环

for循环通用形式如下:

for (init-expr; test-expr; update-expr)

    body-statement

转换成while循环的形式:

init-expr;

while (test-expr) {

body-statement

update-expr;

}

再转换成do-while形式:

init-expr;

if (!test-expr)

goto done;

do {

body-statement

update-expr;

} while (test-expr);

done:

最后转换成对应的goto形式:

init-expr;

t = test-expr;

if (!t)

goto done;

loop:

body-statement

update-expr;

t = test-expr;

if (t)

goto loop;

done:


5.6 条件传送指令

条件传送指令cmov根据条件码选择是否将一个值复制到一个寄存器



5.7 switch语句

编译器会使用跳转表(jump table)来实现switch语句。跳转表是一个数组,表项i是一个代码段地址,当switch的值为i时,程序就指向表项i地址上的代码。比使用分支语句性能好很多。



6. 过程

大多数机器只提供转移控制到过程和从过程中转移出控制的指令,数据传递、局部变量分配和释放通过程序栈完成。

6.1 栈帧结构

栈用于传递过程(函数)参数、存储返回信息、保存寄存器以恢复调用前状态,以及本地存储。为某一个过程(函数)分配的那部分栈称为栈帧(stack frame)。下图为栈帧的通用结构。栈帧的顶端用两个寄存器确定,%ebp为帧指针、%esp为栈指针。



假如过程P(调用者)调用过程Q(被调用者),那么Q的参数存在P的栈帧中,P的返回地址也被压入P的栈中(P栈帧的尾部)。返回地址就是从Q返回到P后继续执行的地方(也就是调用P语句之后的那条语句)。Q的栈帧从保存的帧指针的值(%ebp)开始。

过程Q还用栈保存其他不能放在寄存器中的局部变量

第一个参数放在相对于%ebp偏移量为8的位置处,剩下的参数(假设参数数据大小不超过4)存储在后续的4字节块中,那么第i个参数在相对于%ebp偏移量为4 + 4i的位置。


6.2 转移控制

过程调用和返回的指令


call指令将返回地址压入栈,跳转到被调用过程起始处,返回地址就是调用者中call指令下一条指令。ret指令从栈中弹出返回地址,从此处继续执行。


6.3 寄存器使用惯例

调用过程中寄存器的保存:

%eax、%edx和%ecx由调用者保存和恢复

%ebx、%esi、%edi由被调用者保存和恢复

%ebp和%esp也由被调用者保存和恢复(可以使用leave指令)


7. 数组分配和访问

数组声明:T A[N];

存储器为它分配一个 L*N 字节的连续区域,其中 L 是类型 T 的大小。x 为起始地址(第一个元素地址),那么第 i 个元素地址为 x + L*i。

把起始地址 x 存在 %edx 中,类型大小 T 存在 %ecx 中,我们就可以使用比例变址寻址来访问数组元素:

movl (%edx, %ecx, 4), %eax


7.1 指针运算

整型数组 E 的起始地址和索引 i 分别存放在 %edx 和%ecx中,下图为 E 相关的表达式:



7.2 嵌套数组(多维数组)

一个二维数组:int A[5][3]; 等价于下面的声明:

typedef int row3_t[3];

row3_t A[5];

对于一个二维数组声明:T D[R][C];假设数组起始地址为 x ,数据长度为 L ,元素 A[i][j] 的存储器地址为:&A[i][j] = x + L(C*i + j);


8. 异质的数据结构

C语言中使用struct和union来创建数据类型。


8.1 结构

struct将不同类型的数据结合到一个对象中,存储在连续区域,以偏移来定位结构中的字段(field)。

下面是一个struct声明

struct rec {

int i;

int j;

int a[3];

int *p;

};

它包含2个4字节的int类型,1个3*4字节的int数组,以及一个4字节的地址,一共24个字节:


通过结构开始地址和字段偏移量来访问结构中的字段,假如开始地址存在 %edx 中,那么 (%edx) 就是 i,4(%edx)就是j


8.2 联合

联合允许一个对象里存储不同类型的数据,用不同的字段来引用相同的内存块。

union U3 {

char c;

int i[2];

double v;

};

对于联合类型U3,里面有3个(种)字段,它们长度分别为:1,8,4,,联合的长度就是包含的字段的最长值,所以U3的长度为8。

8.3 数据对齐

某些计算机系统要求某种类型对象的地址必须为某个值(2,4或8)的倍数,这就是数据对齐。数据对齐简化了硬件间的接口设计,并且可以提高存储器性能。

Linux的对齐策略为,2字节数据类型(如short)的地址为2的整数倍,较大的数据类型(如int、int*、float和double)地址必须为4的整数倍。


9. 理解指针

指针映射到机器代码的关键原则:

  • 每个指针都对应一个类型
  • 每个指针都有一个值,为某指定类型对象的地址,NULL表示没指向任何对象
  • 指针用&运算符创建,一般使用指令leal实现
  • 操作符*用于指针的间接引用
  • 数组与指针紧密联系,数组引用(如a[3])与指针间接引用(如*(a+3))效果一样
  • 将指针从一种类型强制转换成另一种类型,只改变它的类型,不改变值
  • 指针可以指向函数:(int)(*fp)(int, int *);







  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值