汇编入门实战:用CPUID指令获取CPU信息

📅 发布时间:2026/10/16 13:00:58
汇编入门实战:用CPUID指令获取CPU信息
1. 从一个获取CPUID的小程序说起1.1 为什么选择CPUID作为汇编入门案例很多人学汇编一上来就被各种寄存器、寻址方式、指令集给劝退了。我当初也是这么过来的抱着厚厚一本指令手册啃了三天合上书脑子里只剩下一堆MOV和ADD。后来我换了个思路——找一个足够小、但又能跑出真实结果的小程序来拆解CPUID获取程序就是最合适的那个。为什么这么说因为它同时满足三个条件第一代码量极小核心逻辑十几行汇编就能搞定第二它必须用汇编来写C语言虽然也能通过内联汇编或者编译器内置函数拿到CPUID但如果你想真正理解CPU是怎么把信息吐出来的绕不开汇编这一层第三它的输出结果是可验证的你跑出来的厂商字符串、型号信息跟系统信息里看到的能对上这种即时反馈对初学者特别友好。CPUID这条指令本身也很有意思。它不是普通的算术或跳转指令而是一条“查询指令”——你往EAX里放一个功能号执行CPUIDCPU就会把对应的信息填到EAX、EBX、ECX、EDX四个寄存器里。这就像你去政务大厅办事先取号EAX放功能号然后窗口把材料递出来四个寄存器返回结果。这个类比虽然粗糙但能帮你快速建立直觉。1.2 这个程序能拿到什么信息具体来说通过CPUID指令我们能获取的信息包括但不限于CPU厂商字符串比如GenuineIntel、AuthenticAMD、处理器型号与家族信息、支持的指令集特性比如SSE、AVX、缓存结构、核心数量、基础频率等等。功能号不同返回的内容也不同。我这次要拆解的程序主要做两件事一是获取厂商字符串二是获取处理器型号和特性标志。厂商字符串的获取方式特别经典——它把EBX、EDX、ECX三个寄存器里的ASCII码拼起来形成一个12字节的字符串。注意这里的顺序不是EAX、EBX、ECX、EDX而是EBX、EDX、ECX这个细节很多人第一次写都会搞错我在后面会详细说。处理器型号和特性标志则通过功能号0x01来获取。EAX返回的是处理器签名里面编码了家族、型号、步进等信息EDX和ECX返回的是特性位图每一位代表一个特性比如第0位是FPU第4位是TSC第25位是SSE第28位是AVX。这些位图信息在写底层优化代码或者做兼容性判断时非常有用。1.3 适合谁来读这篇内容如果你已经学过一点C语言知道什么是寄存器、什么是内存地址但从来没碰过汇编那这篇内容就是为你准备的。我会从最基础的环境搭建开始一步步带你写出一个能跑的CPUID程序然后逐行拆解每条指令的含义。如果你已经写过一些汇编但一直停留在“能跑就行”的阶段对ATT语法和Intel语法的区别、寄存器保存规则、系统调用约定这些细节模棱两可那这篇内容也能帮你把这些坑填上。我会在关键地方对比两种语法的差异并且解释为什么在Linux环境下ATT语法更常见。如果你只是想快速拿到CPU信息不想折腾汇编那我也得提前说清楚——这篇内容不是给你提供现成工具的而是带你理解底层原理的。你可以直接去看/proc/cpuinfo但看完之后你依然不知道那些信息是怎么来的。自己动手写一遍感受完全不一样。2. 动手之前环境与基础知识准备2.1 搭建汇编开发环境在Linux环境下写汇编你需要的工具其实很少。一个文本编辑器、一个汇编器as、一个链接器ld再加上一个调试器gdb这四样东西就够了。大部分Linux发行版默认都装了binutils包里面就包含as和ld。如果你用的是比较精简的系统可能需要手动装一下。我平时用的组合是Vim写代码、GNU as做汇编、ld做链接、gdb做调试。这套组合的好处是全部是命令行工具轻量、可控、没有IDE的干扰。你不需要装什么庞大的开发环境打开终端就能干活。先确认一下工具是否齐全as --version ld --version gdb --version如果都能正常输出版本信息那环境就没问题了。如果提示命令找不到用包管理器装一下binutils和gdb就行。这里有个小细节要注意GNU as默认使用ATT语法这也是我选择用ATT语法来写这个程序的原因。虽然Intel语法对初学者更友好因为它更接近人类的阅读习惯但ATT语法是GNU工具链的原生语法你在阅读Linux内核源码、glibc源码的时候看到的都是ATT语法。早点适应它后面看源码会轻松很多。2.2 ATT语法与Intel语法的核心差异既然要用ATT语法那就得先把它的几个“反直觉”的地方说清楚。我第一次从Intel语法切到ATT语法的时候被操作数顺序搞晕了好一阵。最核心的差异是操作数顺序。Intel语法是“目标在前源在后”比如mov eax, 1是把1放到eax里。ATT语法反过来“源在前目标在后”同样的操作写成movl $1, %eax。这个顺序差异在简单指令里还不容易出错但一旦涉及到复杂的寻址或者多条指令串联搞反了就会写出完全错误的代码。第二个差异是寄存器前缀。ATT语法里所有寄存器前面都要加百分号比如%eax、%ebx。立即数前面要加美元符号比如$1、$0x01。这两个符号是强制性的漏写一个汇编器就会报错。第三个差异是操作数大小后缀。ATT语法在指令后面加后缀来表示操作数宽度b表示字节8位w表示字16位l表示长字32位q表示四字64位。比如movb是移动一个字节movl是移动一个长字movq是移动一个四字。这个后缀在操作数宽度不明确的时候特别重要因为汇编器需要知道你到底要操作多少位的数据。第四个差异是寻址方式的写法。Intel语法的寻址是[base index*scale disp]ATT语法写成disp(base, index, scale)。比如Intel的[ebx ecx*4 8]在ATT里写成8(%ebx, %ecx, 4)。刚开始看会觉得别扭但用多了会发现这种写法其实更规整。我整理了一个对照表方便你快速查阅特性Intel语法ATT语法操作数顺序目标, 源源, 目标寄存器前缀无%立即数前缀无$操作数大小由寄存器推断指令后缀b/w/l/q寻址方式[baseindex*scaledisp]disp(base,index,scale)注释符号;#注意在ATT语法里注释用#号不是分号。分号在ATT语法里是语句分隔符跟C语言里的分号作用类似。这个细节很容易搞混我当初就因为把注释写成了分号汇编器报了一堆莫名其妙的错误。2.3 CPUID指令的基本工作原理CPUID指令的工作方式前面简单提过这里再展开说一下。它是一条“叶子功能”指令——你通过EAX寄存器传入一个功能号也叫叶子号CPU执行CPUID后把结果写到EAX、EBX、ECX、EDX四个寄存器里。不同的功能号返回不同的信息。常用的功能号有这么几个0x00获取厂商字符串和最大功能号。返回的EBX、EDX、ECX拼起来就是12字节的厂商字符串EAX返回的是CPU支持的最大功能号。0x01获取处理器签名和特性标志。EAX返回处理器签名EBX返回附加信息如CLFLUSH大小、逻辑处理器数量等ECX和EDX返回特性位图。0x02获取缓存和TLB描述符。0x03获取处理器序列号早期CPU支持现在基本不用了。0x04获取确定性缓存参数。0x07获取扩展特性标志。0x80000000获取扩展功能号的最大值。0x80000002到0x80000004获取处理器品牌字符串。这个程序主要用到0x00和0x01两个功能号。0x00拿厂商字符串0x01拿型号和特性。够用了先把这两个吃透其他的以后按需查手册就行。提示CPUID指令在早期的CPU上可能不存在。如果你在非常老的硬件上跑这个程序可能会遇到非法指令异常。不过现在市面上能见到的x86处理器基本都支持CPUID这个问题在实际开发中很少遇到。如果你真的需要做兼容性判断可以先检查EFLAGS寄存器的ID位是否可修改以此判断CPUID是否可用。3. 逐行拆解CPUID程序的完整实现3.1 程序整体结构与数据段设计我先把这个程序的完整代码贴出来然后逐段拆解。代码不长但每一行都有讲究。.section .data vendor_str: .ascii Vendor: vendor_len . - vendor_str brand_str: .ascii Brand: brand_len . - brand_str newline: .ascii \n newline_len 1 .section .bss .lcomm vendor_buf, 13 .lcomm brand_buf, 49 .section .text .global _start _start: # 获取厂商字符串 movl $0x00, %eax cpuid movl %ebx, vendor_buf movl %edx, vendor_buf4 movl %ecx, vendor_buf8 movb $0, vendor_buf12 # 输出 Vendor: movl $4, %eax movl $1, %ebx leal vendor_str, %ecx movl $vendor_len, %edx int $0x80 # 输出厂商字符串 movl $4, %eax movl $1, %ebx leal vendor_buf, %ecx movl $12, %edx int $0x80 # 输出换行 movl $4, %eax movl $1, %ebx leal newline, %ecx movl $newline_len, %edx int $0x80 # 获取处理器品牌字符串扩展功能 movl $0x80000000, %eax cpuid cmpl $0x80000004, %eax jb .done movl $0x80000002, %eax cpuid movl %eax, brand_buf movl %ebx, brand_buf4 movl %ecx, brand_buf8 movl %edx, brand_buf12 movl $0x80000003, %eax cpuid movl %eax, brand_buf16 movl %ebx, brand_buf20 movl %ecx, brand_buf24 movl %edx, brand_buf28 movl $0x80000004, %eax cpuid movl %eax, brand_buf32 movl %ebx, brand_buf36 movl %ecx, brand_buf40 movl %edx, brand_buf44 movb $0, brand_buf48 # 输出 Brand: movl $4, %eax movl $1, %ebx leal brand_str, %ecx movl $brand_len, %edx int $0x80 # 输出品牌字符串 movl $4, %eax movl $1, %ebx leal brand_buf, %ecx movl $48, %edx int $0x80 # 输出换行 movl $4, %eax movl $1, %ebx leal newline, %ecx movl $newline_len, %edx int $0x80 .done: # 退出程序 movl $1, %eax xorl %ebx, %ebx int $0x80这个程序的结构很清晰数据段放静态字符串BSS段放缓冲区代码段是主逻辑。我选择用系统调用int 0x80来输出而不是调用C库的printf原因有两个一是避免链接C库程序更独立二是能让你看到Linux系统调用的原始面貌对理解用户态和内核态的交互有帮助。数据段里定义了几个字符串常量用.ascii指令来声明。.ascii不会自动加结尾的零字节所以后面用.-vendor_str来计算长度。这个“点减标签”的写法在汇编里很常见表示当前位置减去标签地址得到的就是字符串的字节数。BSS段用.lcomm来声明未初始化的缓冲区。.lcomm是“local common”的缩写它声明的符号是局部符号不会出现在目标文件的全局符号表里。vendor_buf分配了13字节12字节厂商字符串加1字节结尾零brand_buf分配了49字节48字节品牌字符串加1字节结尾零。3.2 获取厂商字符串的关键细节获取厂商字符串的逻辑是整段代码里最经典的部分也是最容易出错的地方。功能号0x00执行后EBX、EDX、ECX三个寄存器里分别存放了厂商字符串的三个4字节片段。注意顺序是EBX、EDX、ECX不是EBX、ECX、EDX也不是EAX、EBX、ECX。为什么是这个顺序这跟x86架构的历史有关。早期CPUID指令的设计者把厂商字符串按这个顺序分配到寄存器里后来就成了固定标准。Intel和AMD的处理器都遵循这个约定所以你不需要担心兼容性问题。代码里用三条movl指令把三个寄存器的值写到vendor_buf的不同偏移处movl %ebx, vendor_buf movl %edx, vendor_buf4 movl %ecx, vendor_buf8vendor_buf4这种写法在ATT语法里表示“vendor_buf的地址加4个字节”。因为movl操作的是32位数据所以每次偏移4字节。三条指令写完12字节的厂商字符串就完整地落在缓冲区里了。最后用movb $0, vendor_buf12在末尾补一个零字节。这个零字节不是必须的因为我们输出的时候指定了长度是12不依赖零字节来判断字符串结尾。但加上它是个好习惯万一后面要把这个缓冲区当C字符串用没有零字节就会出问题。注意如果你在64位系统上写这个程序寄存器名要改成%rbx、%rdx、%rcx但CPUID指令在64位模式下依然只写低32位高32位会被清零。所以用32位寄存器名也没问题但要注意movl写入内存时只写4字节不会覆盖相邻数据。3.3 品牌字符串的获取与拼接品牌字符串的获取比厂商字符串复杂一些因为它需要调用三个连续的功能号0x80000002、0x80000003、0x80000004。每个功能号返回16字节的数据四个寄存器各4字节三个功能号加起来就是48字节正好是一个完整的品牌字符串。在调用这三个功能号之前先要检查CPU是否支持扩展功能号。用功能号0x80000000查询EAX返回的是支持的最大扩展功能号。如果这个值小于0x80000004说明CPU不支持品牌字符串查询直接跳过。movl $0x80000000, %eax cpuid cmpl $0x80000004, %eax jb .donecmpl是比较指令jb是“小于则跳转”jump if below。这两条指令合起来的意思是如果EAX小于0x80000004就跳到.done标签处退出。这个检查很有必要因为不是所有x86处理器都支持品牌字符串功能。三个功能号的调用逻辑是一样的都是执行CPUID然后把四个寄存器的值依次写入缓冲区。注意写入顺序是EAX、EBX、ECX、EDX跟厂商字符串的EBX、EDX、ECX不同。这个差异要记清楚写反了输出的字符串就是乱码。movl $0x80000002, %eax cpuid movl %eax, brand_buf movl %ebx, brand_buf4 movl %ecx, brand_buf8 movl %edx, brand_buf12后面两个功能号的代码结构完全一样只是目标偏移量不同。0x80000003的结果写到brand_buf16开始的位置0x80000004的结果写到brand_buf32开始的位置。最后在brand_buf48处补一个零字节。品牌字符串里通常包含处理器型号、频率、厂商等信息比如“Intel(R) Core(TM) i7-9750H CPU 2.60GHz”这样的内容。不同CPU返回的字符串长度不一样但缓冲区固定分配48字节不足的部分会用空格填充。3.4 用系统调用输出结果输出部分用的是Linux的write系统调用。系统调用号放在EAX里write的调用号是4。参数依次是EBX放文件描述符1表示标准输出ECX放缓冲区地址EDX放要写入的字节数。然后执行int $0x80触发系统调用。movl $4, %eax movl $1, %ebx leal vendor_str, %ecx movl $vendor_len, %edx int $0x80这里用leal而不是movl来加载地址是因为leal计算的是地址而不是取值。leal vendor_str, %ecx把vendor_str的地址放到ECX里如果用movl vendor_str, %ecx加载的就是vendor_str处的内容也就是字符串的前4个字节那就错了。int $0x80是32位Linux的系统调用入口。在64位系统上传统的int $0x80依然可用但更推荐用syscall指令。不过为了保持代码在32位和64位下的兼容性我这里用的是int $0x80。如果你在64位系统上汇编链接需要加上--32选项来生成32位目标文件或者用syscall重写系统调用部分。程序最后用exit系统调用退出调用号是1EBX放退出码。xorl %ebx, %ebx把EBX清零表示退出码为0正常退出。3.5 汇编、链接与运行代码写完之后保存为cpuid.s然后用as汇编、ld链接as --32 -o cpuid.o cpuid.s ld -m elf_i386 -o cpuid cpuid.o ./cpuid--32选项告诉as生成32位目标文件-m elf_i386告诉ld使用32位ELF格式链接。如果你在64位系统上不加这两个选项链接会报错提示架构不匹配。运行之后你应该能看到类似这样的输出Vendor: GenuineIntel Brand: Intel(R) Core(TM) i7-9750H CPU 2.60GHz厂商字符串和品牌字符串的具体内容取决于你的CPU。如果输出是乱码大概率是寄存器写入顺序搞错了回头检查一下厂商字符串的EBX、EDX、ECX顺序和品牌字符串的EAX、EBX、ECX、EDX顺序。提示如果你在虚拟机里跑这个程序厂商字符串可能会显示虚拟化平台的厂商信息比如“VMwareVMware”或“KVMKVMKVM”。这是正常现象因为虚拟机模拟了CPUID指令的返回结果。4. 实操中踩过的坑与排查技巧4.1 寄存器顺序写反导致的乱码问题我第一次写这个程序的时候厂商字符串输出的是“ineIntGenu”一看就是顺序错了。当时我把EBX、EDX、ECX写成了EBX、ECX、EDX结果第二个4字节和第三个4字节互换了位置。正确的顺序是EBX、EDX、ECX这个顺序没有任何逻辑可言纯粹是历史约定只能死记硬背。品牌字符串的顺序也容易搞错。品牌字符串是EAX、EBX、ECX、EDX跟厂商字符串完全不同。我建议你在写的时候把这两个顺序写在纸上贴在显示器旁边写一次查一次写个三五遍就记住了。排查这类问题的方法很简单如果输出的字符串是乱码但字符集看起来是对的都是可打印字符那基本就是顺序问题。如果输出的是完全不可打印的二进制乱码那可能是地址计算错了或者缓冲区大小不够导致越界覆盖。4.2 系统调用参数设置错误系统调用的参数设置是另一个高频错误点。write系统调用需要三个参数文件描述符、缓冲区地址、写入长度。这三个参数分别放在EBX、ECX、EDX里顺序不能错。我有一次把缓冲区地址放到了EDX里长度放到了ECX里结果程序试图从地址1读取数据直接段错误。还有一个容易忽略的点是系统调用号。write的调用号是4exit的调用号是1。这两个数字要记清楚。如果你不确定某个系统调用的调用号可以查/usr/include/asm/unistd_32.h文件里面列出了所有32位系统调用的编号。grep write /usr/include/asm/unistd_32.h输出应该是#define __NR_write 4。用同样的方法可以查其他系统调用的编号。4.3 32位与64位环境的兼容性问题在64位系统上汇编32位代码最容易遇到的问题就是链接失败。如果你直接用as -o cpuid.o cpuid.s不加--32as会默认生成64位目标文件然后你用ld -o cpuid cpuid.o链接ld会按照64位格式链接但你的代码里用的是32位寄存器和int $0x80链接出来的程序跑不起来。正确的做法是在汇编和链接时都明确指定32位as --32 -o cpuid.o cpuid.s ld -m elf_i386 -o cpuid cpuid.o如果你不想每次都加这些选项可以在汇编文件开头加上.code32指令告诉汇编器生成32位代码。但链接选项-m elf_i386还是得加因为ld需要知道目标格式。另一个方案是直接写64位版本的代码用syscall代替int $0x80用%rax、%rdi、%rsi、%rdx代替%eax、%ebx、%ecx、%edx。64位系统调用的参数传递规则跟32位不同write的参数依次放在RDI、RSI、RDX里调用号放在RAX里。这个方案的好处是不需要特殊链接选项直接as -o cpuid.o cpuid.s和ld -o cpuid cpuid.o就行。4.4 常见问题速查表我把实际写这个程序时遇到的问题整理成了一个速查表方便你快速定位和解决问题现象可能原因解决方法输出乱码但字符可打印寄存器写入顺序错误厂商字符串用EBX、EDX、ECX品牌字符串用EAX、EBX、ECX、EDX输出完全不可打印的二进制缓冲区地址计算错误检查leal指令是否正确加载了地址段错误Segmentation fault系统调用参数放错寄存器write的参数依次是EBX、ECX、EDX链接报错“architecture mismatch”32位代码用了64位链接加--32和-m elf_i386选项程序无输出直接退出品牌字符串检查跳转到了.done检查cmpl和jb指令的比较逻辑输出字符串被截断写入长度设置错误厂商字符串长度12品牌字符串长度48汇编报错“bad expression”注释用了分号而不是#号ATT语法注释用#号注意如果你在运行程序时遇到“Illegal instruction”错误说明你的CPU不支持CPUID指令。这种情况极其罕见但如果真的遇到了可以在程序开头先检查EFLAGS寄存器的ID位确认CPUID可用后再执行。4.5 调试汇编程序的实用技巧调试汇编程序跟调试高级语言程序不太一样你不能靠打印变量来排查问题因为打印本身就需要写一堆汇编代码。我常用的调试手段有这么几个第一个是用gdb单步执行。在gdb里用layout regs打开寄存器窗口用layout asm打开反汇编窗口然后stepi单步执行每执行一条指令就看一下寄存器的变化。这个方法最直观但比较费时间适合排查逻辑错误。第二个是在关键位置插入系统调用输出中间结果。比如在获取厂商字符串之后先把EBX、EDX、ECX的值以十六进制形式打印出来确认它们的内容是否正确。这个方法需要额外写一些输出代码但能快速定位问题出在哪一步。第三个是用objdump反汇编目标文件检查汇编器生成的机器码是否符合预期。有时候汇编器会对指令进行优化或者调整导致实际执行的代码跟你写的代码有出入。objdump -d cpuid.o可以看到每条指令对应的机器码对照Intel手册查一下就能确认。第四个是检查退出码。程序退出时的EBX值就是退出码你可以用echo $?查看。如果程序在某个检查点异常退出把退出码设置成不同的值就能判断程序执行到了哪一步。5. 从CPUID程序延伸出的汇编学习路径5.1 下一步可以练手的汇编项目写完CPUID程序之后你已经掌握了汇编的基本框架数据段定义、BSS段分配、代码段逻辑、系统调用、寄存器操作。接下来可以找一些稍微复杂一点的项目来练手巩固这些知识。我推荐的下一个练手项目是“十六进制转储工具”也就是自己实现一个简化版的hexdump。这个项目需要你读取文件内容、按字节处理数据、格式化输出涉及文件操作、循环、条件判断、位运算等知识点。代码量大概在100到150行左右比CPUID程序复杂但不会复杂到让你无从下手。再下一个项目可以试试“简单的计算器”支持加减乘除四则运算。这个项目需要你解析命令行参数、做数值转换、处理除零错误涉及字符串处理、算术运算、错误处理等内容。写完这个你对汇编的掌控力会上一个台阶。如果你对系统编程感兴趣可以尝试用汇编实现一个简单的shell支持命令执行和管道。这个项目涉及fork、execve、waitpid、pipe等系统调用能让你深入理解进程管理和进程间通信的底层机制。不过这个项目的难度比较大建议先把前两个项目做熟了再挑战。5.2 汇编在实际开发中的应用场景很多人觉得汇编是“屠龙之技”学了没用。其实在不少领域汇编依然是不可或缺的技能。性能敏感的代码路径是一个典型场景。比如加密算法里的核心循环、图像处理里的像素操作、音视频编解码里的变换函数用汇编手写往往能比编译器生成的代码快20%到50%。当然前提是你真的懂CPU的流水线、缓存、分支预测这些底层机制否则手写汇编可能还不如编译器优化得好。逆向工程和漏洞分析是另一个场景。分析二进制程序、调试崩溃、挖掘漏洞都需要你能读懂汇编代码。你可以不会写汇编但一定要会读汇编。CPUID程序虽然简单但它帮你建立了“寄存器-内存-指令”的思维模型这个模型在读汇编的时候非常有用。嵌入式开发和操作系统内核开发也离不开汇编。启动代码、中断处理、上下文切换、原子操作这些地方往往需要用汇编来实现因为高级语言无法精确控制硬件行为。即使你平时用C语言写内核代码遇到这些关键路径时也得切到汇编。5.3 推荐的学习资源和练习方法学习汇编最怕的就是抱着指令手册死记硬背。我的建议是“用中学”——找一个具体的项目边做边查做完一个项目比背一百页手册都管用。指令集手册是必备的参考书但不要从头读到尾。Intel的软件开发手册有几千页你只需要在遇到不认识的指令时去查对应的章节就行。AMD的指令集手册也可以作为补充参考两家在指令编码上基本一致但文档风格不同可以对照着看。练习方法上我推荐“改写练习”——找一段C代码用汇编重写然后对比编译器生成的汇编代码。编译器比如gcc可以用-S选项生成汇编代码你可以把自己写的汇编跟编译器生成的对比看看编译器是怎么优化的自己哪里写得不够好。这个练习方法能快速提升你的汇编水平因为编译器是“不会犯错”的老师它的输出就是标准答案。另外多读优秀的汇编代码也很有帮助。Linux内核的启动代码、glibc的字符串函数、FFmpeg的SIMD优化代码都是很好的学习材料。读的时候不要试图理解每一行先抓住整体结构再逐步深入细节。提示如果你在学汇编的过程中感到挫败这很正常。汇编的细节非常多没有人能一次记住所有东西。我写了这么多年汇编遇到不常用的指令还是得查手册。重要的是理解核心概念——寄存器、内存、指令流、调用约定——这些概念掌握了具体的指令查手册就行。5.4 关于ATT语法和Intel语法的选择建议最后说一下语法选择的问题。ATT语法和Intel语法没有优劣之分只是风格不同。GNU工具链默认用ATT语法所以你在Linux环境下写汇编用ATT语法会更顺手。Windows环境下的汇编器比如MASM、NASM默认用Intel语法如果你主要做Windows开发用Intel语法更自然。我的建议是两种语法都了解一下至少能读懂。因为你在实际工作中可能会遇到各种来源的汇编代码有的用ATT有的用Intel如果只会一种读另一种的时候会很吃力。好在两种语法的差异是固定的那几条规则花半个小时就能搞清楚剩下的就是熟练度问题。如果你实在不习惯ATT语法GNU as也支持Intel语法可以用.intel_syntax noprefix指令切换到Intel模式。但我不推荐这么做因为Linux生态里的汇编代码绝大多数是ATT语法你切换成Intel语法之后反而跟周围的代码风格不一致了。入乡随俗在Linux下就用ATT语法吧。我个人在实际操作中的体会是汇编语言的学习曲线确实比较陡但一旦跨过了入门阶段后面的路会越走越宽。CPUID程序是一个很好的起点它足够小让你能完整地走一遍“写代码-汇编-链接-运行-调试”的流程它又足够真实让你能拿到实际的CPU信息而不是在纸上谈兵。把这个程序吃透你对汇编的理解就已经超过大多数人了。