ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

3.4 Accessing Information(访问信息)

3.4 Accessing Information(访问信息) 第 6 课3.4、3.4.1寄存器与操作数3.4 Accessing Information 概述16 个 64 位通用寄存器%rax %rbx %rcx %rdx %rsi %rdi %rbp %rsp %r8 %r9 %r10 %r11 %r12 %r13 %r14 %r15它们可以保存整数、地址和临时结果。“通用”不表示完全没有特殊用途%rsp是栈指针参数、返回值以及 caller-saved/callee-saved 等角色属于调用约定将在 3.7 学习。同一寄存器的重叠视图以%rax为例名称可见部分大小%rax全部 64 位8 字节%eax%rax的低 32 位4 字节%ax%rax的低 16 位2 字节%al%rax的低 8 位1 字节这些不是四个独立寄存器而是同一个物理架构寄存器的重叠名称。例如%rax 0x1122334455667788 %eax 0x55667788 %ax 0x7788 %al 0x88新增加的%r8至%r15也有同样的低位名称例如%r8d、%r8w、%r8b。写入低位部分时高位如何变化写入目标结果8 位例如%al只改低 8 位高 56 位不变16 位例如%ax只改低 16 位高 48 位不变32 位例如%eax改低 32 位并把高 32 位清零64 位例如%rax覆盖完整 64 位示例初始 %rax 0x1122334455667788 写 %al0xFF - %rax 0x11223344556677FF 写 %eax0xAABBCCDD - %rax 0x00000000AABBCCDD这条 32 位清零规则只针对寄存器目的操作数向内存执行 4 字节写入只改变指定的 4 个内存字节。32 位写入清零的历史与实现意义x86 寄存器从 8086 的AX扩展为 IA-32 的EAX再扩展为 AMD64 的RAX。写AL或AX时保留其他位是早期程序已经能观察并依赖的行为。IA-32 程序原本看不到EAX之上的位因此 AMD64 可以在 64 位模式中规定32 位结果自动零扩展为完整 64 位结果而不改变旧程序可观察的 32 位结果。如果写%eax还要保留旧%rax的高 32 位处理器必须把“旧高位”和“新低位”合并这会让新结果错误地依赖旧%rax。清零高 32 位意味着新%rax完全由当前指令定义有利于寄存器重命名和乱序执行。官方参考AMD64 Architecture Programmer’s Manual规定 32 位 GPR 结果零扩展8 位和 16 位写入保留未写高位。AMD Software Optimization Guide解释部分寄存器合并会产生 false dependency而低 32 位写入因清零高位而不产生这种合并依赖。零扩展不等于符号扩展。比如把 32 位-1写入%eax后完整%rax是0x00000000FFFFFFFF如果需要 64 位有符号的-1需要专门的符号扩展或 64 位操作。3.4.1 Operand Specifiers三类基本操作数类别ATT 例子含义立即数$0x100常数0x100本身寄存器%rax寄存器中保存的值R[%rax]内存(%rax)地址R[%rax]处的内存值M[R[%rax]]美元符号$是区分立即数与内存引用的关键。例如$0x104是数字0x104而裸写0x104表示绝对内存位置M[0x104]。通用有效地址公式最一般的内存形式为Imm(%rb, %ri, s)有效地址是EA Imm R[%rb] R[%ri] × s其中Imm是固定字节偏移量。%rb是 64 位基址寄存器。%ri是 64 位索引寄存器。s是比例因子只能为 1、2、4、8。省略的组成部分按 0 处理省略比例因子时相当于 1。比例因子常对应数组元素大小。例如(%rax,%rdx,4)可以表示“数组基址 下标 × 每个元素 4 字节”。有效地址与操作数值必须分开先计算 EA 再读取 M[EA]例如%rax0x100、M[0x104]0xAB时4(%rax) 的有效地址 0x104 4(%rax) 的操作数值 0xABPractice Problem 3.1 对照已知R[%rax]0x100 R[%rcx]0x1 R[%rdx]0x3 M[0x100]0xFF M[0x104]0xAB M[0x108]0x13 M[0x10C]0x11操作数有效含义或地址值%rax寄存器值0x1000x104M[0x104]0xAB$0x108立即数0x108(%rax)M[0x100]0xFF4(%rax)M[0x104]0xAB9(%rax,%rdx)M[0x10C]0x11260(%rcx,%rdx)2600x104EA0x1080x130xFC(,%rcx,4)EA0x1000xFF(%rax,%rdx,4)EA0x10C0x11本节易错点把%eax、%ax、%al当作互不相关的寄存器。误以为所有部分寄存器写入都保留高位32 位写入会清零高 32 位。把 32 位零扩展误解成符号扩展。忘记$把立即数误读成绝对内存引用。只算出有效地址却没有继续读取M[EA]或反过来把内存值当成地址。把寻址式中的位移量误当成元素数量它本质上是字节偏移量。忘记比例因子只能是 1、2、4、8。第 7 课3.4.2数据传送指令mov的基本复制规则ATT 格式的基本语义是mov S, D D ← S源操作数S在前目的操作数D在后。指令后缀决定一次复制的数据宽度指令宽度movb1 字节movw2 字节movl4 字节movq8 字节普通mov允许以下组合立即数 → 寄存器立即数 → 内存寄存器 → 寄存器寄存器 → 内存内存 → 寄存器目的操作数不能是立即数普通mov也不允许内存直接复制到另一处内存。内存复制为什么需要寄存器中转下面的指令非法movq (%rax), (%rbx)因为源和目的都是内存操作数。应拆成一次加载和一次存储movq (%rax), %rcx movq %rcx, (%rbx)假设%rax中存放地址 A且M[A] 13第一条从地址 A 读取 8 字节把数值 13 放进%rcx。第二条把%rcx中的 13 写到%rbx所指向的位置。%rax和%rbx中的地址本身没有被改写。这是普通 x86-64mov的指令编码约束。寄存器在两条指令之间充当临时中转站。数据宽度必须匹配对普通mov而言参与数据复制的源和目的必须与后缀宽度一致movb %al, %dl movw %ax, %dx movl %eax, %edx movq %rax, %rdx例如movl %rax, (%rsp) # 错误movl要复制 32 位数据而%rax是 64 位数据寄存器。可按意图改成movl %eax, (%rsp) # 写 4 字节 movq %rax, (%rsp) # 写 8 字节这里%rsp只是用来计算内存地址仍应使用 64 位名称它不需要和movl的 32 位数据宽度一致。同理movb %si, 8(%rbp) # 错误%si 是 16 位 movw %si, 8(%rbp) # 正确复制 16 位 movb %sil, 8(%rbp) # 正确复制 %rsi 的低 8 位寄存器写入与 64 位立即数**在 x86-64 中任何产生 32 位寄存器结果的指令都会把对应 64 位寄存器的高 32 位清零。**因此movl %eax, %edx不仅复制 EAX 的低 32 位还会得到RDX ZeroExtend(EAX)但如果目的操作数是内存movl就只写 4 字节不会额外清零相邻内存。任意 64 位立即数movabsq普通movq的立即数编码通常使用符号扩展后的 32 位立即数。若要把任意 64 位立即数放入寄存器可使用movabsq $0x1122334455667788, %raxmovabsq的目的操作数只能是寄存器。零扩展movz零扩展把较小的源复制到较大的目的并把新增的高位全部补 0。指令名中的两个后缀分别表示源宽度和目的宽度movzbq %al, %rbx # byte → quad word movzwl %ax, %edx # word → long word若%al 0xFFmovzbq %al, %rbx执行后%rbx 0x00000000000000FF源可以是寄存器或内存目的必须是寄存器。为什么没有movzlq**指令集中没有专门的movzlq因为 32 位寄存器写入本来就会自动清零高 32 位。**需要把 EAX 零扩展到 RDX 时直接写movl %eax, %edx执行后 RDX 的高 32 位自动变成 0。符号扩展movs与cltq符号扩展把源操作数的最高位也就是符号位重复填充到新增的高位movsbq %al, %rbx movswq %ax, %rbx movslq %eax, %rdx若%al 0x80按 8 位有符号数解释就是 -128。执行movsbq %al, %rbx结果为%rbx 0xFFFFFFFFFFFFFF80新增位全是二进制 1因此每四个二进制 1 写成一个十六进制数字F。普通mov的数据两端同宽movz和movs则有源、目的两个宽度且目的比源大。cltqcltq专门把 EAX 符号扩展到 RAXcltq效果等价于movslq %eax, %rax它不显式写操作数因为源和目的寄存器已经由指令固定。零扩展与符号扩展对比假设EAX 0xFFFFFFFF执行零扩展式复制movl %eax, %edx得到RDX 0x00000000FFFFFFFF执行符号扩展movslq %eax, %rdx得到RDX 0xFFFFFFFFFFFFFFFF位模式相同的 32 位源采用哪一种扩展方式决定新增的高 32 位是 0 还是符号位。Practice Problem 3.2根据操作数选择后缀指令骨架正确后缀理由mov_ %eax, (%rsp)movlEAX 是 32 位mov_ (%rax), %dxmovwDX 是 16 位mov_ $0xFF, %blmovbBL 是 8 位mov_ (%rsp,%rdx,4), %dlmovbDL 是 8 位mov_ (%rdx), %raxmovqRAX 是 64 位mov_ %dx, (%rax)movwDX 是 16 位Practice Problem 3.3常见错误错误写法问题一种修正movb $0xF, (%ebx)64 位模式下地址寄存器应使用 64 位名称movb $0xF, (%rbx)movl %rax, (%rsp)%rax与movl数据宽度不匹配movl %eax, (%rsp)movw (%rax), 4(%rsp)内存 → 内存先读入 16 位寄存器再写回movb %al, %sl不存在%sl按意图使用%sil或其他 8 位寄存器movq %rax, $0x123立即数不能作目的操作数让目的成为寄存器或内存movl %eax, %rdx普通movl的目的应是 32 位名称movl %eax, %edxmovb %si, 8(%rbp)%si是 16 位movw %si, 8(%rbp)或movb %sil, 8(%rbp)易错点把括号中的地址寄存器宽度误认为数据宽度例如movl %eax, (%rsp)中数据宽度是 32 位而地址仍由 64 位%rsp提供。忘记普通mov的数据源和数据目的必须同宽。把普通mov与movz/movs混为一谈后两者故意让源和目的宽度不同。把符号扩展新增的二进制 1 写成十六进制数字1正确写法应为F。忘记 32 位寄存器写入会清零对应 64 位寄存器的高 32 位。第 8 课3.4.3数据传送示例exchange读出旧值、写入新值、返回旧值C 代码longexchange(long*xp,longy){longx*xp;*xpy;returnx;}对应汇编# xp in %rdi, y in %rsi exchange: movq (%rdi), %rax movq %rsi, (%rdi) ret这里%rdi保存指针xp也就是一个地址。(%rdi)表示访问该地址处的值对应 C 表达式*xp。%rsi保存参数y。%rax保存函数返回值。第一条指令把*xp的旧值读入%raxmovq (%rdi), %rax它同时实现了long x *xp并为最终的return x准备好返回值。第二条指令只把y写入xp指向的位置movq %rsi, (%rdi)它不会修改%rax因此函数最终返回的是写入前的旧值。具体执行轨迹假设%rdi 0x1000 %rsi 3 M[0x1000] 4执行第一条后%rax 4 M[0x1000] 4执行第二条后%rax 4 M[0x1000] 3因此longa4;longbexchange(a,3);执行结果为a 3 b 4指针和局部变量在机器级代码中的样子C 指针在机器级代码中就是地址解引用指针就是把地址放入寄存器再用括号形式访问内存。局部变量不一定拥有独立的内存位置。这里的局部变量x一直保存在%rax中。寄存器访问通常比内存访问快编译器会尽量把短期使用的局部值保存在寄存器中。Practice Problem 3.4读取、转换、写回通用操作src_t*sp;dest_t*dp;*dp(dest_t)*sp;约定sp在%rdi中、dp在%rsi中。每组都分成两步从(%rdi)读取源值必要时完成扩展并把结果放入 RAX 的适当部分。按目的类型的宽度把 RAX 的适当部分写入(%rsi)。src_tdest_t第一条读取并转换第二条按目的宽度写回longlongmovq (%rdi), %raxmovq %rax, (%rsi)charintmovsbl (%rdi), %eaxmovl %eax, (%rsi)charunsignedmovsbl (%rdi), %eaxmovl %eax, (%rsi)unsigned charlongmovzbq (%rdi), %raxmovq %rax, (%rsi)intcharmovl (%rdi), %eaxmovb %al, (%rsi)unsignedunsigned charmovl (%rdi), %eaxmovb %al, (%rsi)charshortmovsbw (%rdi), %axmovw %ax, (%rsi)同时改变宽度和有无符号性C 的转换规则是先改变宽度再改变有无符号解释。因此char → unsigned仍要先进行符号扩展movsbl (%rdi), %eax movl %eax, (%rsi)例如有符号char -1的位模式为0xFF符号扩展0xFF → 0xFFFFFFFF 再解释为 unsigned4294967295若误用movzbl会得到0x000000FF 255不符合 C 的转换语义。缩小转换不需要扩展。例如int → char先读入完整 32 位值再只写最低 8 位movl (%rdi), %eax movb %al, (%rsi)Practice Problem 3.5三值循环轮换汇编代码# xp in %rdi, yp in %rsi, zp in %rdx decode1: movq (%rdi), %r8 movq (%rsi), %rcx movq (%rdx), %rax movq %r8, (%rsi) movq %rcx, (%rdx) movq %rax, (%rdi) ret若原来*xp A *yp B *zp C前三条先保存全部旧值%r8 A %rcx B %rax C后三条再依次写回最终得到*xp C *yp A *zp B等价 C 代码voiddecode1(long*xp,long*yp,long*zp){longx*xp;longy*yp;longz*zp;*ypx;*zpy;*xpz;}它不是只交换两个位置而是让三个位置的值按xp → yp → zp → xp循环轮换。易错点把xp和*xp混淆前者是地址后者是地址处的值。误以为exchange返回写入后的新值实际上旧值已经提前保存在%rax中。误以为写内存的movq %rsi, (%rdi)会同时修改%rax。看到目的类型是unsigned就直接零扩展忽略了char → unsigned要先按源类型符号扩展到目标宽度。第一条已经完成扩展后第二条仍误用movs第二条只需按目的类型宽度使用普通mov。分析多次内存写入时忽略前三条指令已经把所有旧值保存在寄存器中。第 9 课3.4.4栈数据的压入与弹出栈的基本模型栈是内存中的一片区域按后进先出last in, first outLIFO的方式管理数据pushq把一个 8 字节值压入栈。popq从栈顶取出一个 8 字节值。%rsp保存当前栈顶元素的地址。x86-64 的栈向低地址方向增长因此压栈会减小%rsp出栈会增大%rsp。书上的图把地址增大的方向画成向上所以视觉上“栈顶”位于图的下方。pushq与popq的两步效果pushq S语义为R[%rsp] ← R[%rsp] - 8 M[R[%rsp]] ← S它等价于subq $8, %rsp movq S, (%rsp)执行顺序必须记清先让%rsp减少 8得到新的栈顶地址。再把 8 字节的源操作数写到新栈顶。假设%rsp 0x108 %rax 0x123执行pushq %rax结果为%rsp 0x100 M[0x100] 0x123popq先读栈顶再加栈指针popq D语义为D ← M[R[%rsp]] R[%rsp] ← R[%rsp] 8它等价于movq (%rsp), D addq $8, %rsp执行顺序为先从当前(%rsp)读取 8 字节值。再让%rsp增加 8使下一个元素成为新栈顶。承接前面的状态%rsp 0x100 M[0x100] 0x123执行popq %rdx结果为%rdx 0x123 %rsp 0x108出栈、内存残留与栈顶含义popq不会把原栈顶的内存字节清零。执行上面的popq后通常仍有M[0x100] 0x123但它已经不再是当前栈中的有效元素因为%rsp已经变成0x108。栈的逻辑边界由%rsp决定而不是由内存中是否残留旧位模式决定。以后执行新的pushq时这块位置可以被覆盖。区分栈顶地址与栈顶值若%rsp 0x100 M[0x100] 0x123则%rsp 0x100 # 地址 (%rsp) 0x123 # 地址处的栈顶值因此movq (%rsp), %rdx得到的是0x123不是地址0x100。后进先出与栈中寻址假设初始%rsp 0x200依次执行pushq %rax # %rax 保存 A pushq %rbx # %rbx 保存 B结果为%rsp 0x1F0 M[0x1F0] B # 当前栈顶最后压入 M[0x1F8] A # 第二个栈元素此时执行popq会先取出B。也可以使用普通内存寻址访问栈中的其他元素movq 8(%rsp), %rdx有效地址为0x1F0 8 0x1F8所以它把A读入%rdx并且不会改变%rsp。易错点看到%rsp 0x100就误认为movq (%rsp), %rdx会把0x100写入%rdx括号表示读取地址处的值。认为“向下增长”是数值向下写在纸上这里的“向下”专指地址数值变小。把pushq的顺序记反正确顺序是先减%rsp再写新栈顶。把popq的顺序记反正确顺序是先读取旧栈顶再增加%rsp。认为popq会擦除原内存它只移动栈的逻辑边界。忘记pushq和popq在本节中操作的都是 8 字节四字quad word。
返回列表