本篇博文为学习《深入理解计算机系统》过程中写的笔记,其中一些图片也摘自本书
首先,概述一下GCC编译器驱动程序把高级语言转换为机器语言的总过程。( 预处理器,编译器,汇编器,连接器的概念和区别)
本篇文章针对其中.s文件即汇编程序进行学习。在学习ATT汇编之前,推荐先去了解一下计算机组成原理这门课(这里是我的学习霸笔记)
一、得到汇编文本的具体操作
使用命令gcc -Og -S hello.c
,就能得到hello.s
,其中编译选项-Og
告诉编译器使用会生成符合原始C代码整体结构的机器代码的优化等级
删除以“.”开头的附加项后得到纯净的汇编代码
(默认64位应用程序,也可以在后面加上-m32指定编译为32位应用程序)
二、ATT汇编代码
具体可阅《深入理解计算机系统》(CSAPP)读书笔记
例题:
void swap(int *a,int *b)
{
int temp=*a;
*a=*b;
*b=temp;
}
int main()
{
int array[10]={10,9,8,7,6,5,4,3,2,1};
for(int i=0;i<10;i++)
{
for(int j=0;j+1<10;j++)
{
if(array[j]>array[j+1])
{
swap(&array[j],&array[j+1]);
}
}
}
for(int i=0;i<10;i++)
{
printf("%d",array[i]);
}
return 0;
}
对应ATT汇编格式
swap:
movl (%rdi), %eax # 将a指针指向的值加载到寄存器eax中
movl (%rsi), %edx # 将b指针指向的值加载到寄存器edx中
movl %edx, (%rdi) # 将edx中的值存储到a指针指向的位置
movl %eax, (%rsi) # 将eax中的值存储到b指针指向的位置
ret # 返回
main:
pushq %rbp # 保存调用者的栈帧指针
pushq %rbx # 保存调用者的寄存器
subq $56, %rsp # 分配56字节的局部变量空间
movl $10, (%rsp) # 初始化数组元素,这里的数据是int类型的,用编码后缀为“l”来表示4字节整形
movl $9, 4(%rsp)
movl $8, 8(%rsp)
movl $7, 12(%rsp)
movl $6, 16(%rsp)
movl $5, 20(%rsp)
movl $4, 24(%rsp)
movl $3, 28(%rsp)
movl $2, 32(%rsp)
movl $1, 36(%rsp)
movl $0, %ebp # 初始化外层循环计数器ebp
jmp .L3 # 跳转到外层循环
.L4:
movl %ebx, %eax # 将内层循环计数器ebx的值复制到eax
addl $1, %eax #内层循环计数器加1
.L6: # 补全的内容在这!!!
cmpl $8, %eax # 比较eax和8
jg .L12 # 如果eax大于8,跳转到.L12结束内层循环
cmpl (%rsp,%rax,4), (%rsp,%rax,4) # 比较array[j]和array[j+1]
je .L4 # 如果相等,跳转到.L4继续内层循环
leaq (%rsp,%rax,4), %rdi # 将array[j]的地址存储到rdi,用leaq是因为这里传的是地址而不是数据
leaq 4(%rsp,%rax,4), %rsi # 将array[j+1]的地址存储到rsi
call swap # 调用swap函数交换array[j]和array[j+1]的值
jmp .L4 # 继续内层循环
.L12:
addl $1, %ebp # 外层循环计数器ebp加1
.L3:
cmpl $9, %ebp # 比较ebp和9,这里ebp代表i
jg .L13 # 如果ebp > 9,则跳转到.L13
movl $0, %eax # 将eax置零,这里eax表示j
jmp .L6 # 跳转到.L6,继续内层循环
.L13:
movl $0, %ebx # 初始化内层循环计数器ebx
jmp .L7 # 跳转到.L7
.L8:
movslq %ebx, %rax # 将数组索引ebx扩展为64位存储在rax中
movl (%rsp,%rax,4), %esi # 将数组索引为ebx的元素加载到esi中
leaq .LC0(%rip), %rdi # 将格式字符串地址加载到rdi中
movl $0, %eax # 将eax置零
call printf@PLT # 调用printf函数输出结果
addl $1, %ebx # 内层循环计数器ebx加1
.L7:
cmpl $9, %ebx # 比较ebx和9
jle .L8 # 如果ebx <= 9,则跳转到.L8,继续循环
movq 40(%rsp), %rax # 恢复栈帧指针
subq %fs:40, %rax # 检查栈是否被破坏
jne .L14 # 如果栈被破坏,则跳转到.L14
movl $0, %eax # 返回0
addq $56, %rsp # 释放局部变量空间
popq %rbx # 恢复调用者的寄存器
popq %rbp # 恢复调用者的栈帧指针
ret # 返回
用C语言goto版本描述上述汇编过程
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int arr[] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
int i, j;
i = 0;
goto L3; // 跳转到外层循环起始位置
L3:
if (i > 9) // 外层循环结束条件
goto L13; // 跳转到循环结束后的位置
j = 0;
goto L6; // 跳转到内层循环起始位置
L6:
if (j > 8) // 内层循环结束条件
goto L4; // 跳转到内层循环结束后的位置
swap(&arr[j], &arr[j + 1]); // 调用swap函数进行交换
j++;
goto L6; // 跳转回内层循环起始位置
L4:
i++;
goto L3; // 跳转回外层循环起始位置
L13:
for (j = 0; j <= 9; j++) {
printf("%d ", arr[j]);
}
printf("\n");
return 0;
}
-
main后为什么需要 pushq %rbp 和 %rbx?
1、寄存器%rbp和%rbx被划分为被调用者寄存器,用于保存上个函数的基址指针和非易失性寄存器的值。简而言之,保存%rbp和%rbx寄存器的值目的是为了保护这些寄存器的值,防止在函数执行期间被修改
2、pushq %rbp
将rbp寄存器的值压入栈中。这是为了保存调用者函数的栈底指针,以便在函数结束时恢复调用者的栈帧。
3、push %rbx
将rbx寄存器的值压入栈中。这是为了保存rbx寄存器的值,以便在函数结束时恢复rbx的原始值。rbx寄存器在函数内部可能会被使用和修改,所以在函数结束时需要恢复原值。
4、在函数结束时,通过popq指令可以将之前保存在栈中的%rbp和%rbx的值恢复回来,以确保函数结束后栈帧的正确恢复。(.L7中体现) -
ATT汇编中,过程调用的本质和约定?
1、栈帧:过程调用时,使用栈来储存相关的数据,如函数的局部变量、参数值、返回地址(像push %rbp
再pop %rbp
)等信息,创建栈一般是在函数开头有个像本例题中subq $56, %rsp
movl $10, (%rsp)
…的操作,销毁时再addq $56, %rsp
加回去,释放空间
2、调用和返回:用call
指令先将返回地址压入栈,并跳转到过程的入口。在返回过程中ret
,从栈中弹出返回地址,并跳转回调用处的下一条指令(下图讲得更清楚)
ebp
:(base pointer )可称为“帧指针”或“基址指针”
esp
:(stack pointer)可称为“ 栈指针”
图片出处
3、参数传递和返回值:传参对应的寄存器分别为:第一个参数%rdi
,第二个参数%rsi
…返回值一般在%rax