同步操作将从 deepinwiki/wiki 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
汇编语言对应机器语言,是程序语言最底层的,且人类可读的形式。汇编在 c 函数和调试的时候都可能涉及到。一些底层的系统调用,需要通过汇编来书写。因此,就算很少使用,也应该对其有一些了解。
常用的是 gcc 项目的 gdb 调试器,和 llvm 项目的 lldb 调试器。lldb 命令兼容 gcc。
zig cc main.c # 编译
lldb main # 调试 mian 程序,然后进入调试的交互模式,q 退出
r # 运行
file a.out # 加载 a.out 程序
r # 运行
b mian.c:18 # 在对应的源文件 18 行设置断点
b main # 对 mian 函数设置断点
r # 停在断点
br list # 断点的列表b
br del 1 # 删除断点(序号)
w s v a # 监视 a 的值发生变化
w l # 监视列表
w del 1 # 删除监视项
bt # 显示调用栈
f 1 # 选择调用栈序号 1,即上一个调用者
v # 显示局部变量
re r # 显示寄存器
p a # 计算表达式,打印 a 的值
d # 反汇编
f # 源码
s # 执行下一步(会进入下一个层级)
si # 执行下一条指令
n # 跳到下一步
ni # 跳到下一步(指令级)
c # 下一个断点
kill # 关闭当前运行的线程
help n # 显示 n 指令的帮助文档,了解更多参数使用方法
q # 退出交互模式
脚本:
target stop-hook add # 暂停时执行以下脚本
re r rbp rsp # 显示寄存器
d -p # 显示反汇编
DONE # 结束
callee saved 表示由被调用函数负责保存和恢复的寄存器,即函数的上下文。即函数调用后,必须保证 rbx,rsp,rbp,r12,r13,r14,r15 不变。
程序按习惯分为多种段落
栈的方向是从高到低,比如栈底是3,下一个地址就是2,1,0。堆就是夹在中间的内存可用地址,一般需要借助系统 api 动态申请。重点是对栈的理解,因为函数调用使用的是栈空间。
栈是一种后进先出的结构,一般而言,栈基(栈底)是高地址,栈顶是低地址。入栈表示将数据添加到栈顶,栈顶 - 1,出栈表示栈顶 + 1,表示删除栈顶位置的数据。刚开始栈顶指向占底,随着数据增多,栈顶地址变小,又随着数据减少,栈顶变大。就是这么一个动态变化。
栈的总体大小是预先分配的。栈顶最大不能超出(栈底 - 大小),最小不能超出栈底。
函数和栈的关系非常密切,所以需要深入的理解汇编是如何实现函数调用过程的。
函数调用实际上有多种协议(约定),c 语言约定是:
简而言之,调用者首先入栈参数(少于6个时使用寄存器)然后跳转到函数(该指令功能上等于入栈当前指令指针加 jmp)。
然后就到了函数内部,首先保存调用者的栈帧指针 rbp,再设置 rbp 等于当前 rsp 栈顶。因为 rsp 总是指向栈顶,它是一个不断变化的指针,所以需要 rbp 栈帧指针作为一个相对定位的基地址指针。
可以理解为每个函数,都有自己的基地址,然后编译器就可以相对定位参数和内部变量的位置,从而逻辑上解耦了调用者和自身的关系。所以可以从任意地方调用该函数,只要按约定提供参数即可。这个相对独立的函数空间,被命名为栈帧(栈片段)。
然后是给函数分配局部变量的空间(局部变量都是可以提前计算总大小和定位相对位置的,编译器可以一次性用 sub $num, %rsp 立即数来分配完整的空间),运行完毕设置返回值,一般设置给 rax。出栈局部空间(只需要用 rbp->rsp即可),然后弹出之前保存的 rbp,用来恢复调用者的栈帧指针。最后是跳转调用处(这条指令相当于弹出 rip,然后 jmp 到该位置)。
因为 c 语言是调用者预先准备参数的,所以回到调用处,调用者就应该自己清理这些参数,将其出栈(add 参数占用, %rsp)。
仔细理顺这一个过程(可以先看完其余部分再回来跟踪这一个过程),作为程序员来说,可以不了解多少汇编指令,因为很少人需要用汇编来写复杂的程序(调试跟踪或作为关键部分少量使用),但是对函数调用这一个过程是需要认知清晰的。
以上是 32 位堆栈的情况,在 64 位第一个入栈参数是 16(%rbp),第二个是 24(%rbp) 依此类推。
汇编除了指令,还有一些元信息,他们给汇编器提供构造信息。比如分段,相对地址和符号的绑定等。
汇编语言和 cpu 架构有关,因为不同架构指令不同。也和汇编器使用的语法有关。比如 intel 汇编格式和 AT&T 格式虽然生成的指令一样,但写法有一些差异。这里主要使用 AT&T 汇编(linux 常见格式)。
main:
: 符号 main 是一个起始地址,由汇编器计算实际地址length = . - buffer
: 点代表当前地址, $length 计算上一个变量长度*%rax
: 取地址,而不是读写指针,如 jmp *%rax
读取 rax 中的值,而非将 rax 视为指针,执行取指针操作特征 | AT&T | Intel |
---|---|---|
方向 | 源,目的 | 目的,源 |
立即数 | $1 | 1 |
进制 | $0xf2 | f2h |
寄存器 | %rax | rax |
指针 | -4(%rbp) | [rbp - 4] |
# ata.S
.section .data
output:
.string "hello asm!\n"
length = .-output
.section .text
.global _start
_start:
mov $1, %rdi
mov $output, %rsi
mov $length, %rdx
mov $1, %rax
syscall
mov $0, %rdi
mov $60, %rax
syscall
# 使用 zig cc 编译器(需要安装 zig)
zig cc ata.S -o ata -nolibc # 编译且不要链接 c 标准库
高级语言很多支持内联汇编,从而让高级语言可以在高级语言代码中临时使用,这可能是程序员使用汇编比较常见的方式。
gcc 内联汇编语法(zig llvm):
// 基本语法
asm volatile("汇编文本"); // volatile 别优化,inline 内联, goto 支持标签
// 扩展语法
asm volatile(
"模板"
: // 输出 (请查看后续详细说明)
: // 输入
: // 破坏影响
);
// zig 编程语言使用的内联样式
var result = asm volatile(
"call printf" // 调用 c 函数 printf
: [ret]"+{rax}"(->i32) // 返回值,类型 i32
: [x]"{rdi}" ("print number=%d \n"), // 第一个参数
[num]"{rsi}" (123), [zero] "{rax}" (0) // 第二个参数, printf 要求 rax = 0
: "rcx" // 笔者估计 printf 内部会使用的寄存器
); // 模板并没有使用变量,所以 x num 之类的标签只是为了满足语法格式要求
模板:
%[符号]
: 引用命名变量输入输出:
[符号] 约束 (变量 或 表达式)
"[标签]"
:指定和命名变量位置相同破坏影响: 内联汇编除了输出外还影响到的其他地方,需要提供给编译器做优化。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。