1、GCC 内嵌汇编.txt24 生活如海,宽容作舟,泛舟于海,方知海之宽阔;生活如山,宽容为径,循径登山,方知山之高大;生活如歌,宽容是曲,和曲而歌,方知歌之动听。 内核代码绝大部分使用 C 语言编写,只有一小部分使用汇编语言编写,例如与特定体系结构相关的代码和对性能影响很大的代码。GCC 提供了内嵌汇编的功能,可以在 C 代码中直接内嵌汇编语言语句,大大方便了程序设计。一、基本内嵌汇编GCC 提供了很好的内嵌汇编支持,最基本的格式是:_asm_ _volatile_(汇编语句模板);1、_asm_asm_是 GCC 关键字 asm 的宏定义:#define _asm_ asm_asm_或 as
2、m 用来声明一个内嵌汇编表达式,所以任何一个内嵌汇编表达式都是以它开头的,是必不可少的。2、汇编语句模板“汇编语句模板”是一组插入到 C 程序中的汇编指令(可以是单个指令,也可以是一组指令)。每条指令都应该由双引号括起,或者整组指令应该由双引号括起。每条指令还应该用一个定界符结尾。有效的定界符为换行符(n)和分号(;)。n 后可以跟一个制表符(t)作为格式化符号,增加 GCC 在汇编文件中生成的指令的可读性。上述原则可以归结为:任意两个指令间要么被分号(;)分开,要么被放在两行;放在两行的方法既可以通过n 的方法来实现,也可以真正的放在两行;可以使用一对或多对双引号,每对双引号里可以放任意多条
3、指令,所有的指令都必须放到双引号中。在基本内嵌汇编中, “汇编语句模板”的书写的格式和你直接在汇编文件中使用汇编语言编程没有什么不同,你可以在其中定义标号(Label),定义对齐(.align n),定义段(.section name)。例如:_asm_(“.align 2nt“ “movl %eax, %ebxnt“ “test %ebx, %ecxnt“ “jne errornt“ “stint“ “error: popl %edint“ “subl %ecx, %ebx“);建议大家都使用这种格式来写内嵌汇编代码。3、_volatile_volatile_是 GCC 关键字 volati
4、le 的宏定义:#define _volatile_ volatile_volatile_或 volatile 是可选的。如果不想让 GCC 的优化改动你的内嵌汇编代码,你最好在前面都加上_volatile_。二、带 C 语言表达式的内嵌汇编在内嵌汇编中,可以将 C 语言表达式指定为汇编指令的操作数,而且不用去管如何将 C 语言表达式的值读入哪个寄存器,以及如何将计算结果写回 C 变量,你只要告诉程序中 C 语言表达式与汇编指令操作数之间的对应关系即可,GCC 会自动插入代码完成必要的操作。 通常嵌入到 C 代码中的汇编语句很难做到与其它部分没有任何关系,因此更多时候需要用到扩展的内嵌汇编格式
5、:_asm_ _volatile_(汇编语句模板 : 输出部分 : 输入部分 : 破坏描述部分);内嵌汇编表达式包含 4 个部分,各部分由“:”分隔。这 4 个部分都不是必须的,任何一个部分都可以为空,其规则为:如果“破坏描述部分”为空,则其前面的“:”必须省略。比如:_asm_(“mov %eax, %ebx“ : :);。如果“汇编语句模板”为空,则“输出部分” , “输入部分”以及“破坏描述部分”可以不为空,也可以为空。比如:_asm_(“ : : : “memory“);。 如果“输出部分” , “输入部分”以及“破坏描述部分”都为空, “输出部分”和“输入部分”之前的“:”既可以省略
6、,也可以不省略。如果都省略,则此汇编退化为一个基本内嵌汇编,否则,仍然是一个带有 C 语言表达式的内嵌汇编。如果“输入部分”和“破坏描述部分”为空,但“输出部分”不为空, “输入部分”前的“:”既可以省略,也可以不省略。如果后面的部分不为空,而前面的部分为空,则前面的“:”都必须保留,否则无法说明不为空的部分究竟是第几部分。如果“破坏描述部分”不为空,而“输出部分”和“输入部分”都为空,则“输出部分”和“输入部分”前的“:”都必须保留。从上面的规则可以看到另外一个事实,区分一个内嵌汇编是基本格式的还是扩展格式的,其规则在于在“汇编语句模板”后面是否有“:”的存在,如果没有则是基本格式的,否则,
7、就是扩展格式的。这两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能使用一个%,这一点和原生汇编相同;而扩展格式则要求寄存器前必须使用两个%。比如:_asm_(“mov %eax, %ebx“ :)和_asm_(“mov %eax, %ebx“)都是正确的写法,而_asm_(“mov %eax, %ebx“ :)和_asm_(“mov %eax, %ebx“)都是错误的写法。任何只带一个“%”的标识符都看成是操作数,而不是寄存器。 1、内嵌汇编举例使用内嵌汇编,要先编写汇编语句模板,然后将 C 语言表达式与指令的操作数相关联,并告诉 GCC 对这些操作有哪些约束条件。例如在下面的汇编语句
8、: _asm_(“movl %1, %0“ : “=r“(result) : “m“(input); “movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作数,称为占位符,内嵌汇编靠它们将 C 语言表达式与指令操作数相对应。指令模板后面用圆括号括起来的是 C 语言表达式,本例中只有两个:“result”和“input” ,他们按照在输出部分和输入部分出现的顺序分别与指令操作数“%0” , “%1”对应;注意对应顺序:第一个 C 语言表达式对应“%0” ;第二个表达式对应“%1” ,依次类推。在每个操作数前面有一个用双引号括起来的字符串,字符串的内容是对该操作数的约束或者说要求。
9、“result”前面的约束字符串是“=r” ,其中“=”表示“result”在指令中是只写的(输出操作数), “r”表示需要将“result”与某个通用寄存器相关联,先将操作数的值读入寄存器,然后在指令中使用相应寄存器,而不是“result”本身,当然指令执行完后需要将寄存器中的值存入变量“result” ,从表面上看好像是指令直接对“result”进行操作,实际上 GCC 做了隐式处理,这样我们可以少写一些指令。 “input”前面的“r”表示该表达式需要先放入某个寄存器,然后在指令中使用该寄存器参加运算。由此可见,C 语言表达式或者变量与寄存器的关系由 GCC 自动处理,我们只需使用约束字
10、符串指导 GCC 如何处理即可。内联汇编的重要性体现在它能够灵活操作,而且可以使其输出通过 C 变量显示出来。因为它具有这种能力,所以_asm_可以用作汇编指令和包含它的 C 程序之间的接口。2、汇编语句模板操作数C 语言表达式可用作内嵌汇编中的汇编指令的操作数。在汇编指令通过对 C 语言表达式进行操作来执行有意义的作业的情况下,操作数是扩展格式的内嵌汇编的主要特性。每个操作数都由操作数约束字符串指定,后面跟着用圆括号括起来的 C 语言表达式,例如:“constraint“(C expression)操作数约束的主要功能是确定操作数的寻址方式。占位符在扩展格式的内嵌汇编的“汇编语句模板”中,操
11、作数由占位符引用。如果总共有 n 个操作数(包括输入和输出),那么第一个输出操作数的编号为 0,逐项递增,总操作数的数目限制在 10 个(%0、%1、%9)。如果要处理很多输入和输出操作,数字型的占位符很快就会变得混乱。为了使条理清晰,GNU 编译器(从版本 3.1 开始)允许声明替换的名称作为占位符。替换的名称在“输入部分”和“输出部分”中声明。格式如下:name “constraint“(C expression)声明 name 后,使用%name的形式替换内嵌汇编代码中相应的数字型占位符。如下面所示:_asm_(“cmoveq %1, %2, %result“: result “=r“(
12、result): “r“(test), “r“(new), “result“(old);在内嵌汇编中使用占位符表示的操作数,总被视为 long 型(4 个字节) ,但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是高字节。方法是在%和序号之间插入一个字母,“b”代表低字节, “h”代表高字节,例如:%h1。必须使用占位符的情况:我们看一看下面这个例子:_asm_(“addl %1, %0“: “=a“(out): “m“(in1), “a“(in2);首先,我们看一看上例中的第 1 个输入操作表达式“m“(in1
13、),它被 GCC 替换之后,表现为addl address_of_in1, %eax,in1 的地址是什么?编译时才知道。所以我们完全无法直接在指令中去写出 in1 的地址,这时使用占位符,交给 GCC 在编译时进行替代,就可以解决这个问题。所以这种情况下,我们必须使用占位符。其次,如果上例中的输出操作表达式“=a“(out)改为“=r“(out),那么 out 究竟会使用哪个寄存器只有到编译时才能通过 GCC 来决定,既然在我们写代码的时候,我们不知道究竟哪个寄存器被选择,我们也就不能直接在指令中写出寄存器的名称,而只能通过占位符替代来解决。3、输出部分“输出部分”用来指定当前内嵌汇编语句的
14、输出。我们看一看这个例子:_asm_(“movl %cr0, %0“ : “=a“(cr0);这个内嵌汇编语句的输出部分为“=r“(cr0),它是一个“操作表达式” ,更具体地在这里叫作“输出操作表达式” ,指定了一个输出操作。 “输出操作表达式”由两部分组成,这两部分都是必不可少的:圆括号括起来的部分是一个 C 语言表达式,用来保存内嵌汇编的一个输出值,其操作就等于 C 的赋值表达式 cr0 = output_value,因此,圆括号中的输出表达式只能是 C 的左值表达式。那么右值 output_value 从何而来呢?答案是双引号中的内容,被称作“操作约束”(Operation Const
15、raint),在这个例子中操作约束为“=a“,它包含两个约束:等号(=)和字母 a,其中等号(=)说明圆括号中左值表达式cr0 是 Write-Only 的,只能够被作为当前内嵌汇编的输出,而不能作为输入。而字母 a 是寄存器 EAX/AX/AL 的简写,说明 cr0 的值要从 EAX 寄存器中获取,也就是说 cr0 = %eax,最终这一点被转化成汇编语句就是 movl %eax, address_of_cr0。另外,需要特别说明的是,很多文档都声明,所有输出操作的操作约束必须包含一个等号(=),但 GCC 的文档中却很清楚的声明,并非如此。因为等号(=)约束说明当前的表达式是Write-O
16、nly 的,但另外还有一个符号加号(+)用来说明当前表达式是 Read-Write 的,如果一个操作约束中没有给出这两个符号中的任何一个,则说明当前表达式是 Read-Only 的。因为对于输出操作来说,肯定是必须是可写的,而等号(=)和加号(+)都表示可写,只不过加号(+) 同时也表示是可读的。所以对于一个输出操作来说,其操作约束只需要有等号(=)或加号(+)中的任意一个就可以了。二者的区别是:等号(=)表示当前操作表达式指定了一个纯粹的输出操作,而加号(+)则表示当前操作表达式不仅仅只是一个输出操作还是一个输入操作。但无论是等号(=)约束还是加号(+)约束所约束的操作表达式都只能放在“输出
17、部分”中,而不能被用在“输入部分”中。在“输出部分”中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。4、输入部分“输入部分”的内容用来指定当前内嵌汇编语句的输入。我们看一看这个例子:_asm_(“movl %0, %db7“ : : “a“(cpu-db7);例中“输入部分”的内容为一个表达式“a“(cpu-db7),被称作“输入操作表达式” ,用来表示一个对当前内嵌汇编的输入。像输出操作表达式一样,一个输入操作表达式也分为两部分:带圆括号的部分(cpu-db7)和带双引号的部分“a“。这两部分对于一个内嵌汇编输入操作表达式来说也是必不可少的。圆括号中的表达式 cpu-db
18、7 是一个 C 语言的表达式,它不必是一个左值表达式,也就是说它不仅可以是放在 C 赋值操作左边的表达式,还可以是放在 C 赋值操作右边的表达式。所以它可以是一个变量,一个数字,还可以是一个复杂的表达式。比如上例可以改为:_asm_(“movl %0, %db7“ : : “a“(foo);_asm_(“movl %0, %db7“ : : “a“(0x1000);_asm_(“movl %0, %db7“ : : “a“(x*y/z);双引号中的部分是约束部分,和输出操作表达式约束不同的是,它不允许指定加号(+)约束和等号(=)约束,也就是说它只能是默认的 Read-Only 的。约束中必须
19、指定一个寄存器约束,例中的“a“表示当前输入变量 cpu-db7 要通过寄存器%eax 输入到当前内嵌汇编中。在“输入部分”中可以有多个输入操作表达式,多个操作表达式中间必须用逗号(,)分开。5、操作约束前面提到过,在内嵌汇编中的每个操作数都应该由操作数约束字符串描述,后面跟着用圆括号括起来的 C 语言表达式。操作数约束主要是确定指令中操作数的寻址方式。约束也可以指定:是否允许操作数位于寄存器中,以及它可以包括在哪些类型的寄存器中操作数是否可以是内存引用,以及在这种情况下使用哪些类型的寻址方式操作数是否可以是立即数约束字符必须与指令对操作数的要求相匹配,否则产生的汇编代码将会有错,在这个例子中
20、:_asm_(“movl %1,%0“ : “=r“(result) : “r“(input);如果将那两个“r“,都改为“m“(“m”表示操作数是内存引用)编译后得到的结果是: movl input, result很明显这是一条非法指令(mov 不允许内存到内存的操作)。每一个输入和输出操作表达式都必须指定自己的操作约束,下面是在 80x86 平台上可能使用的操作约束:寄存器约束当你当前的输入或输出需要借助一个寄存器时,你需要为其指定一个寄存器约束。你可以直接指定一个寄存器的名字,比如:_asm_(“movl %0, %cr0“ : : “eax“(cr0);也可以指定一个缩写,比如:_as
21、m_(“movl %0, %cr0“ : : “a“(cr0);如果你指定一个缩写,比如“a” ,则 GCC 将会根据当前操作表达式中 C 语言表达式的类型决定使用%eax,还是%ax 或%al。比如:unsigned short shrt;_asm_(“mov %0,%bx“ : : “a“(shrt);由于变量 shrt 是 16-bit short 类型,则编译出来的汇编代码中,会让此变量使用%ax 寄存器。无论是输入还是输出的操作表达式,都可以使用寄存器约束。内存约束 如果一个输入或输出操作表达式的 C 语言表达式表现为一个内存地址,并且不想借助于任何寄存器,则可以使用内存约束。比如:
22、_asm_(“lidt %0“ : “=m“(idt_addr);使用内存方式进行输入输出时,由于不借助寄存器,所以 GCC 不会按照你的声明对其作任何的输入输出处理。GCC 只会直接拿来用,究竟对这个 C 语言表达式而言是输入还是输出,完全依赖与你写在“汇编语句模板”中的指令对其操作的指令。当操作数位于内存中时,任何对它们执行的操作都将在内存位置中直接发生,所以,对于内存约束类型的操作表达式而言,放在“输入部分”还是放在“输出部分” ,对编译结果是没有任何影响的,既然对于内存约束类型的操作表达式来说,GCC 不会自动为它做任何事情,那么放在哪儿也就无所谓了。但从程序员的角度而言,为了增强代码
23、的可读性,最好能够把它放在符合实际情况的地方。立即数约束如果一个输入或输出操作表达式的 C 语言表达式是一个数字常数,并且不想借助于任何寄存器,则可以使用立即数约束。由于立即数在 C 中只能作为右值,所以对于使用立即数约束的操作表达式而言,只能放在“输入部分” 。比如:_asm_(“movl %0, %eax“ : : “i“(100);匹配约束匹配约束符是一位数字:“0” , “1”, “9”,表示它约束的 C 表达式分别与占位符%0,%1,%9 相对应的 C 变量匹配。例如使用“0”作为%1 的约束字符,那么%0 和%1 表示同一个 C 变量。 在某些情况下,一个变量既要充当输入操作数,也
24、要充当输出操作数。可以通过使用匹配约束在内嵌汇编中的“输入部分”指定这种情况。 _asm_(“incl %0“ : “=a“(var) : “0“(var);在上面的示例中,寄存器%eax 既用作输入变量,也用作输出变量。将输入变量 var 读取到%eax,执行 inc 指令后将更新了值的%eax 再次存储在 var 中。这里的“0“指定与第 0 个输出变量相同的约束。即,它指定 var 的输出实例只应该存储在%eax 中。该约束可以用于以下情况:输入从变量中读取,或者变量被修改后,修改写回到同一变量中不需要将输入操作数和输出操作数的实例分开使用匹配约束最重要的意义在于它们可以导致有效地使用可
25、用寄存器。i386 指令集中许多指令的操作数是读写型的,例如:addl %1, %0它先读取%0 与%1 原来的值然后将两者的值相加,并把结果存回%0,因此操作数%0 是读写型操作数。老版本的 GCC 对这种类型操作数的支持不是很好,它将操作数严格分为输入和输出两种,分别放在输入部分和输出部分,而没有一个单独部分描述读写型操作数。_asm_(“addl %1, %0“ : “=r“(result) : “r“(input);上例使用“r”约束的输出变量,GCC 会分配一个寄存器,然后用该寄存器替换占位符,但是在使用该寄存器之前并不将 result 变量的值先读入寄存器,GCC 认为所有输出变量
26、以前的值都没有用处,也就没有必要将其读入寄存器(这可能是因为 AT产生的代码很可能不对。看上去上面的代码可以正常工作,因为我们知道%0 和%1 都和 result 相关,应该使用同一个寄存器,但是 GCC 并不去判断%0 和%1 是否和同一个 C 语言表达式或变量相关联(这样易于产生与内嵌汇编相应的汇编代码),因此%0 和%1 使用的寄存器可能不同。使用匹配约束符后,GCC 知道应将对应的操作数放在同一个位置(同一个寄存器或者同一个内存变量)。使用匹配约束字符的代码如下:_asm_(“addl %2,%0“ : “=r“(result) : “0“(result), “m“(input);相应
27、的汇编代码为: movl $0, _result movl $1, _input movl _result, %edx movl %edx, %eax #APP addl _input, %eax #NO_APP movl %eax, %edx movl %edx, _result 可以看到与 result 相关的寄存器是%edx,在执行指令 addl 之前先从%edx 将 result 读入%eax,执行之后需要将结果从%eax 读入%edx,最后存入 result 中。这里我们可以看出 GCC处理内嵌汇编中输出操作数的一点点信息:addl 并没有使用%edx,可见它不是简单的用result
28、 对应的寄存器%edx 去替换%0,而是先分配一个寄存器,执行运算,最后才将运算结果存入对应的变量,因此 GCC 是先看该占位符对应的变量的约束符,发现是一个输出型寄存器变量,就为它分配一个寄存器,此时没有去管对应的 C 变量,最后 GCC 知道还要将寄存器的值写回变量,与此同时,它发现该变量与%edx 关联,因此先存入%edx,再存入变量。在新版本的 GCC 中增加了一个约束字符“+” ,它表示操作数是读写型的,GCC 知道应将变量值先读入寄存器,然后计算,最后写回变量,而无需在输入部分再去描述该变量。_asm_(“addl %1, %0“ : “+r“(result) : “m“(inpu
29、t);产生的汇编代码如下: movl $0,_result movl $1,_input movl _result,%eax #APP addl _input,%eax #NO_APP movl %eax,_result L2: movl %ebp,%esp 处理的比使用匹配约束符的情况还要好,省去了好几条汇编代码。修饰符等号(=)和加号(+)用于对输出操作表达式的修饰,一个输出操作表达式要么被等号(=)修饰,要么被加号(+)修饰,二者必居其一。使用等号(=)说明此输出操作表达式是 Write-Only 的,使用加号(+)说明此输出操作表达式是 Read-Write 的。它们必须是输出操作表达
30、式约束字符串中的第一个字符。比如:“a=“(var)是非法的,而“+g“(var)则是合法的。当使用加号(+)的时候,此输出操作表达式等价于使用等号(=)约束再加上一个输入操作表达式。比如:_asm_(“incl %0“ : “+a“(var);等价于_asm_(“incl %0“ : “=a“(var) : “0“(var);像等号(=)和加号(+)修饰符一样,符号( movl %edx, %1“ : “=a“(ret) : “r“(bar);我们知道函数的 int 型返回值存放在%eax 中,但是 GCC 编译的结果是输入和输出同时使用了寄存器%eax,如下:movl bar, %eax#
31、APPcall foomovl %ebx, %eax#NO_APPmovl %eax, ret结果显然不对,原因是 GCC 并不知道%eax 中的值是我们所要的。避免这种情况的方法是使用“ movl %edx, %1“ : “=6、破坏描述部分有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间计算结果,这样就难免会破坏原有寄存器的内容。如果希望 GCC 在编译时能够将这一点考虑进去。那么你就可以在“破坏描述部分”声明这些寄存器或内存。这种情况一般发生在一个寄存器出现在“汇编语句模板” ,但却不是由输入或输出操作表达式所指定的,也不是在一些输入或输出操作表
32、达式使用“r“、“g“约束时由 GCC 为其选择的,同时此寄存器被“汇编语句模板”中的指令修改,而这个寄存器只是供当前内嵌汇编临时使用的情况。比如:_asm_(“movl %0, %ebx“ : : “a“(foo) : “%ebx“);寄存器%ebx 出现在“汇编语句模板”中,并且被 movl 指令修改,但却未被任何输入或输出操作表达式指定,所以你需要在“破坏描述部分”指定“%ebx“,以让 GCC 知道这一点。因为你在输入或输出操作表达式所指定的寄存器,或当你为一些输入或输出操作表达式使用“r“、“g“约束,让 GCC 为你选择一个寄存器时,GCC 对这些寄存器是非常清楚的它知道这些寄存器
33、是被修改的,你根本不需要在“破坏描述部分”再声明它们。但除此之外,GCC 对剩下的寄存器中哪些会被当前的内嵌汇编修改一无所知。所以如果你真的在当前内嵌汇编语句中修改了它们,那么就最好“破坏描述部分”中声明它们,让 GCC 针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。在“破坏描述部分”中指定这些寄存器的方法很简单,你只需要将寄存器的名字使用双引号引起来。如果有多个寄存器需要声明,你需要在任意两个声明之间用逗号隔开。比如:_asm_(“movl %0, %ebx; popl %ecx“ : : “a“(foo) : “%ebx“, “%ecx“ );注意准备在
34、“破坏描述部分”声明的寄存器必须使用完整的寄存器名称,在寄存器名称前面使用的“%”是可选的。另外需要注意的是,如果你在“破坏描述部分”声明了一个寄存器,那么这个寄存器将不能再被用做当前内嵌汇编语句的输入或输出操作表达式的寄存器约束,如果输入或输出操作表达式的寄存器约束被指定为“r“或“g“,GCC 也不会选择已经被声明在“破坏描述部分”中的寄存器。比如:_asm_(“movl %0, %ebx“ : : “a“(foo) : “%eax“, “%ebx“);此例中,由于输出操作表达式“a“(foo)的寄存器约束已经指定了%eax 寄存器,那么再在“破坏描述部分”中指定“%eax“就是非法的。编
35、译时,GCC 会给出编译错误。除了寄存器的内容会被改变,内存的内容也可以被修改。如果一个“汇编语句模板”中的指令对内存进行了修改,或者在此内嵌汇编出现的地方内存内容可能发生改变,而被改变的内存地址你没有在其输出操作表达式使用“m“约束,这种情况下你需要在“破坏描述部分”使用字符串“memory“向 GCC 声明:“在这里,内存发生了或可能发生了改变” 。例如:void * memset(void * s, char c, size_t count)_asm_(“cldnt“repnt“stosb“: /* no output */: “a“(c), “D“(s), “c“(count): “%
36、ecx“, “%edi“, “memory“);return s;此例实现了标准函数库 memset,其内嵌汇编中的 stosb 对内存进行了改动,而其被修改的内存地址 s 被指定装入%edi,没有任何输出操作表达式使用了“m“约束,以指定内存地址 s处的内容发生了改变。所以在其“破坏描述部分”使用“memory“向 GCC 声明:内存内容发生了变动。如果一个内嵌汇编语句的“破坏描述部分”存在“memory“,那么 GCC 会保证在此内嵌汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内嵌汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。因为这个时候寄存器中的拷贝已经很可能和内存处的内容不一致了。当一个“汇编语句模板”中包含影响 eflags 寄存器中的条件标志,那么需要在“破坏描述部分”中使用“cc“来声明这一点。这些指令包括 adc,div,popfl,btr,bts 等等,另外,当包含 call 指令时,由于你不知道你所 call 的函数是否会修改条件标志,为了稳妥起见,最好也使用“cc“。