本节摘要:汇编程序不是指令的平铺,而是按段(section)归类的结构体:代码进 .text、已初始化数据进 .data、零初始化数据进 .bss、只读常量进 .rodata;符号(symbol)则是跨段引用的"命名锚点",链接器靠符号表把散落各处的名字接上地址。本节拆开程序骨架的这两个承重件,给出一套可复用的程序模板,并用 readelf 亲眼验证段与权限的对应关系。读完你能写出结构规范的手写汇编程序,并解释"为什么数据不能放代码段"这类问题。
看懂一份汇编源码的关键动作,不是逐行翻译指令,而是先扫一遍结构:哪几个段、哪些符号、数据在哪儿。这个习惯来自一个朴素的工程事实——操作系统装载程序时不是整块搬进内存,而是按段授予不同权限:代码段映射为可读可执行、不可写,数据段可读可写、不可执行,只读段干脆连写门都不留。你写的每一行 .text、.data 声明,最终都变成了内存页上的权限位。骨架不清晰,轻则程序一跑就崩(写到了只读页),重则给攻击者留门(把可写和可执行混在同一页,正是第 7 章要讲的经典漏洞土壤)。
符号是第二个承重件。汇编源码里的标签(label)看似只是行尾的冒号名,实际会被汇入目标文件的符号表,成为链接器拼接模块时的接头暗号——你在 1.3 节见过的 undefined reference 报错,就是符号表对不上号时的呐喊。
先认识段的分工。.text 存机器码,权限可读可执行;.data 存已初始化的全局数据(比如带初值的查找表),可读可写;.bss 存零初始化数据——它的妙处是不占文件空间,装载时操作系统直接给一块清零内存,几百 KB 的零数组在文件里只占一个长度记录;.rodata 存常量(字符串字面量、常量矩阵),可读不可写。C 编译器产物里你还会见到更多花样(.eh_frame、.plt 之类),但手写汇编阶段这四段覆盖全部需求。
数据声明靠 db/dw/dd/dq 四兄弟(按字节、字、双字、四字定义)与 resb 系列预留区,配合 times 复制与 align 对齐:
section .data matrix: dd 1, 2, 3, 4 ; 四个32位:一张小表 title: db "asm", 0 ; 字符串补零结尾 scale: equ 8 ; 汇编期常量,不占内存 section .bss buffer: resb 256 ; 预留256字节,文件里不占空间 aligned: align 16 ; 对齐到16字节(SSE加载需要) resd 4
equ 与 db 的区别值得点破:equ 是汇编期的文字替换,不产生任何字节;db 系列真正往段里写数据。混淆两者的代价是莫名多出的"神秘字节"——排查半天发现是把常量声明成了数据。
标签三问:它是什么、谁能看见、去了哪里。它是什么——标签记录"定义处"的地址,汇入符号表;谁能看见——默认只在文件内有效(局部符号),被 global 声明后对外可见(全局符号),链接器据此跨文件解析;去了哪里——函数符号进符号表供链接与调试,纯内部标签(如循环头 .loop)可以留在文件内不外泄。
用 readelf 与 nm 把这些名词变成可见物:
$ nasm -f elf64 demo.asm -o demo.o $ readelf -S demo.o | grep -E "Name|text|data|bss" [Nr] Name Type Address Off Size ES Flg [ 1] .text PROGBITS 00000000 040 1c 00 AX [ 3] .data PROGBITS 00000000 060 10 00 WA [ 4] .bss NOBITS 00000000 070 100 00 WA $ nm demo.o 0000000000000000 T sum3 0000000000000010 t .loop
三处值得驻足的细节:.text 的标志是 AX(可分配可执行、不可写),.data 是 WA(可写、不可执行)——上一节说的权限位在这里现形;.bss 的类型是 NOBITS,意思是"文件里没有实体",坐实了零页不占文件空间的论断;nm 输出里大写 T 是全局符号(对外可见)、小写 t 是局部符号,.loop 这类内部标签果然没有外泄。第 7 章逆向分析时,符号表就是攻击者眼中的地图——strip 掉符号表的程序难读十倍,原因正在于此。

把段与符号的知识合拢成一个骨架模板,全册后续手写实验都从这个模板改:
; template.asm —— 全册通用骨架 section .rodata prompt: db "value: ", 0 ; 常量放只读段 section .data counter: dd 0 ; 带初值的可写数据 section .bss scratch: resq 8 ; 零初始化的暂存区 section .text global myfunc myfunc: push rbp ; 栈帧开场(第 4 章详解) mov rbp, rsp ; —— 函数体:只动 rax 及调用者易失寄存器 —— pop rbp ret
模板里藏着三条纪律。第一,数据按"只读与否、初值有无"归段,别把字符串丢进 .data——不仅浪费可写页,还会让只读优化失效。第二,全局符号最小化:只 export 必须被外部调用的函数,其余一律局部,符号表越瘦越安全。第三,函数开场先建栈帧、结尾先拆栈帧,这一对括号式的动作在第 4 章栈帧解剖里是绝对主角,现在先机械照做。
⚠️ 常见坑:把字符串字面量放进 .data 后又在运行时写入,程序在部分系统上直接段错误——因为链接器/装载器可能把相邻的只读数据合并成同一只读页。字符串一律进 .rodata,需要修改的缓冲区去 .bss 申请。
💡 关键直觉:段是"权限的最小分配单位"。你声明段的方式,本质是在替操作系统填写内存保护清单——把段声明当安全设计来做,而不只是语法填空。
骨架齐了,下一节沿构建管线走全程:目标文件里那张"待补清单"如何被链接器兑现。