从C程序中读取标志寄存器

8
为了满足好奇心,我正在尝试读取标志寄存器并以一种好的方式打印出来。
我尝试使用gcc的 asm 关键字读取它,但是我无法使其工作。有什么提示如何做到这一点吗?我正在运行Intel Core 2 Duo和Mac OS X。以下是我拥有的代码。我希望它能告诉我是否发生了溢出:
#include <stdio.h>

int main (void){
  int a=10, b=0, bold=0;
  printf("%d\n",b);
  while(1){
    a++;
  __asm__ ("pushf\n\t"
   "movl 4(%%esp), %%eax\n\t"
   "movl %%eax , %0\n\t"
   :"=r"(b)      
   :         
   :"%eax"        
   ); 
  if(b!=bold){ 
    printf("register changed \n %d\t to\t %d",bold , b);
  }
  bold = b;
  }
}

这会导致段错误。运行gdb时,出现以下信息:
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x000000005fbfee5c
0x0000000100000eaf in main () at asm.c:9
9       asm ("pushf \n\t"

有关纯汇编问题,请参见:https://dev59.com/_3M_5IYBdhLWcg3wXx5N - Ciro Santilli OurBigBook.com
6个回答

7
这可能是 XY问题的情况。要检查溢出,您不需要获取硬件溢出标志,因为该标志可以轻松地从符号位计算得出。
一个例子是在使用8位寄存器时,将127和127相加会发生什么。127+127等于254,但使用8位算术,结果将是1111 1110二进制,即-2的补码,因此为负数。正操作数(或反之)的负结果称为溢出。然后,溢出标志将被设置,以便程序可以知道问题并缓解此问题或发出错误信号。当两个具有相同符号的数字相加(或两个具有相反符号的数字相减)时,最高有效位(在这里视为符号位)改变时,溢出标志就会被设置。如果两个加法操作数的符号不同(或两个减法操作数的符号相同),则永远不会发生溢出。

内部上,溢出标志通常由符号位的进位和退位异或生成。由于符号位与被认为无符号的数字的最高有效位相同,因此在添加或减去无符号数字时,“溢出”标志是“无意义的”,通常被忽略。

https://en.wikipedia.org/wiki/Overflow_flag

C语言中的实现如下:

int add(int a, int b, int* overflowed)
{
    // do an unsigned addition since to prevent UB due to signed overflow
    unsigned int r = (unsigned int)a + (unsigned int)b;

    // if a and b have the same sign and the result's sign is different from a and b
    // then the addition was overflowed
    *overflowed = !!((~(a ^ b) & (a ^ r)) & 0x80000000);
    return (int)r;
}

这种方式可以在任何架构上 便携地 运行,而你的解决方案只能在x86上运行。聪明的编译器可以识别该模式并在可能的情况下改用溢出标志。在大多数RISC架构(如MIPS或RISC-V)中,没有标志,所有有符号/无符号溢出必须通过分析符号位来在软件中进行检查。
一些编译器具有检查溢出的内置函数,例如ClangGCC中的__builtin_add_overflow。使用该内置函数,您还可以在非标志体系结构上轻松查看计算溢出的方式。例如,在ARM上,操作如下:
add     w3, w0, w1  # r = a + b
eon     w0, w0, w1  # a = a ^ ~b
eor     w1, w3, w1  # b = b ^ r
str     w3, [x2]    # store sum ([x2] = r)
and     w0, w1, w0  # a = a & b = (a ^ ~b) & (b ^ r)
lsr     w0, w0, 31  # overflowed = a >> 31
ret

which是我上面写的内容的变体。
另请参阅:
对于无符号整数,这要容易得多。
unsigned int a, b, result = a + b;
int overflowed = (result < a);

*overflowed = !!((~(a ^ b) 中的双重否定是故意的还是拼写错误? - Crouching Kitten
2
@CrouchingKitten 这是有意的。这是将一个值强制转换为0和1的标准方法,在Linux中被广泛使用。在C中,!!(x)的含义是什么(尤其是在Linux内核中)? - phuclv

6
您可以使用PUSHF/PUSHFD/PUSHFQ指令(详见http://siyobik.info/main/reference/instruction/PUSHF%2FPUSHFD)将标志寄存器推入堆栈中。从那里,您可以在C语言中对其进行解释。或者您可以直接测试(针对无符号算术的进位标志或有符号算术的溢出标志),并进行分支。
(具体来说,要测试溢出位,您可以使用JO(设置时跳转)和JNO(未设置时跳转)进行分支-它是寄存器中第11位(从0开始计数))
关于EFLAGS位布局:http://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture#EFLAGS_Register 一个非常简陋的Visual C语法测试(只是wham-bam/一些跳转到调试流程),因为我不知道GCC语法:
int test2 = 2147483647; // max 32-bit signed int (0x7fffffff)
unsigned int flags_w_overflow, flags_wo_overflow;
__asm
{
    mov ebx, test2 // ebx = test value

    // test for no overflow
    xor eax, eax // eax = 0
    add eax, ebx // add ebx
    jno no_overflow // jump if no overflow

testoverflow:
    // test for overflow
    xor ecx, ecx // ecx = 0
    inc ecx // ecx = 1
    add ecx, ebx // overflow!
    pushfd // store flags (32 bits)
    jo overflow // jump if overflow
    jmp done // jump if not overflown :(

no_overflow:
    pushfd // store flags (32 bits)
    pop edx // edx = flags w/o overflow
    jmp testoverflow // back to next test

overflow:
    jmp done // yeah we're done here :)

done:
    pop eax // eax = flags w/overflow
    mov flags_w_overflow, eax // store
    mov flags_wo_overflow, edx // store
}

if (flags_w_overflow & (1 << 11)) __asm int 0x3 // overflow bit set correctly
if (flags_wo_overflow & (1 << 11)) __asm int 0x3 // overflow bit set incorrectly

return 0;

我必须承认我不能真正阅读GCC内联语法,但请确保在赋值之后放置pushf指令。我将在VCC中进行测试。哦,还有,使用PUSHFD来获取完整的32位。 - nielsj
我无法使用PUSHFD:它在64位模式下不受支持。至少这是gcc告诉我的 ;) 。在哪个赋值之后我应该推送? - Benedikt Bergenthal
我不太清楚,但也有一个64位的变体:PUSHFQ。通常,您会在影响所需位的算术操作之后尽快放置push指令。就像我提到的条件分支JO一样。请检查我的编辑。 - nielsj
@BenediktWutzi:如果你想进行这些黑客技巧,请将你的增量移动到内联汇编块中,以汇编语言编写。 - ninjalj
siyobik的链接已经失效。 - hlitz

5
编译器可以重新排列指令,因此您不能依赖于lahf紧接着增量的位置。实际上,可能根本没有增量。在您的代码中,您不使用a的值,因此编译器可以完全将其优化掉。
因此,要么用汇编写增量+检查,要么用C语言写。
此外,lahf仅从eflags中加载ah(8位),而溢出标志位在ah之外。最好使用pushf; pop %eax
一些测试:
#include <stdio.h>

int main (void){
    int a=2147483640, b=0, bold=0;
    printf("%d\n",b);
    while(1){
            a++;
            __asm__ __volatile__ ("pushf \n\t"
                            "pop %%eax\n\t"
                            "movl %%eax, %0\n\t"
                            :"=r"(b)
                            :
                            :"%eax"
                    );
            if((b & 0x800) != (bold & 0x800)){
                    printf("register changed \n %x\t to\t %x\n",bold , b);
            }
            bold = b;
    }
}


$ gcc -Wall  -o ex2 ex2.c
$ ./ex2  # Works by sheer luck
0
register changed
 200206  to      200a96
register changed
 200a96  to      200282

$ gcc -Wall -O -o ex2 ex2.c
$ ./ex2  # Doesn't work, the compiler hasn't even optimized yet!
0

更加实用的答案,很好 :) 我愚蠢地忽略了LAHF指令(我也从未真正使用过该指令)。 - nielsj
1
我认为 #define overflow32(a,b,c) \ ( ( ((a)>>31)==((b)>>31) ) && ( ((a)>>31)!=((c)>>31) ) ) 可以用于在 C 语言中进行加法溢出检查。 - ninjalj
看起来还好。整个问题让我想到:你想要明确检查这种情况的是什么样的情形?我的意思是,当然这可能会发生,但通常逻辑应该先发制人地避免这种情况发生(如果不需要的话)。另一方面,有些非常严格的软件(例如航空电子设备等)已经有套件进行了极限测试以针对这些情况生成C代码。 - nielsj
@nj:嗯,我从我曾经工作过的一个MIPS仿真核心中学到了这个知识,那是在侏罗纪时期,当我有空闲时间时(MIPS具有带溢出异常的有符号算术)。 - ninjalj
是的,那就说得通了 - 我从来没有编写过MIPS汇编器,也很少编写MIPS目标代码 :) 而且x86汇编器至少也有4年了。 - nielsj
没有必要使用单独的 mov 和硬编码的 %%eax,只需使用 pop %0。但请注意,在具有红区(x86-64 System V)的ABI中,这样做是不安全的,因为您无法告诉编译器您破坏了 %rsp 下方的内存。因此,64位版本需要首先执行 sub $128, %rsp或者,如果您只想检查溢出条件,请使用带有8位输出寄存器的 seto %0 或在GCC6语法中,使用标志输出条件。当然,所有这些都是无意义的,因为无法保证 a++ 的编译方式;GCC可能已经使用了LEA或将其优化/转换为其他内容。 - Peter Cordes

3
您不能假设GCC如何实现a++操作,或者它是否在内联汇编之前进行了计算,或者在函数调用之前进行了计算。
您可以将a设置为内联汇编的(未使用的)输入,但是gcc仍然可以选择使用lea进行复制和添加,而不是incadd,或者在内联后进行常量传播可能会将其转换为mov-immediate。
当然,gcc还可以执行一些在内联汇编之前写入FLAGS的其他计算。
没有办法使a++; asm(...)对此安全。
立即停止,你走错了。 如果坚持使用asm,则需要在asm内部执行addinc以便您可以读取flags输出。如果只关心溢出标志,可以使用SETCC,具体来说是seto%0,以创建一个8位输出值。或者更好的做法是,使用GCC6标志输出语法告诉编译器布尔输出结果位于内联asm结尾处的FLAGS中的OF条件中。
此外,C中的有符号溢出是未定义行为,因此实际上在a++中引起溢出已经是错误了。如果在事后检测到它,它通常不会表现出来,但是如果您将a用作数组索引或其他gcc可能已将其扩展为64位以避免重新进行符号扩展。
自gcc5以来,GCC有用于检测溢出的内置函数。
有内置的对带符号/无符号加、减和乘的版本,请参见GCC手册,避免了有符号溢出UB并告诉你是否发生了溢出。
  • bool __builtin_add_overflow (type1 a, type2 b, type3 *res) 是通用版本
  • bool __builtin_sadd_overflow (int a, int b, int *res) 是带符号的int版本
  • bool __builtin_saddll_overflow (long long int a, long long int b, long long int *res) 是带符号的64位long long版本。
编译器将尝试在可能的情况下使用硬件指令来实现这些内置函数,例如在加法后根据溢出条件跳转,根据进位条件跳转等。

如果您需要在目标平台上操作不同大小的long类型,可以使用版本。 (对于x86-64 gcc,int始终是32位,long long始终是64位,但long取决于Windows与非Windows。 对于像AVR这样的平台,int将是16位,只有long是32位。)

int checked_add_int(int a, int b, bool *of) {
    int result;
    *of = __builtin_sadd_overflow(a, b, &result);
    return result;
}

使用gcc -O3编译x86-64 System V的内容生成此汇编代码,在Godbolt上查看

checked_add_int:
        mov     eax, edi
        add     eax, esi             # can't use the normal lea eax, [rdi+rsi]
        seto    BYTE PTR [rdx]
        and     BYTE PTR [rdx], 1    # silly compiler, it's already 0/1
        ret

ICC19使用setcc将结果存储到一个整型寄存器中,然后再存储该寄存器,从uops方面来说效果相同,但代码体积更大。

在内联到调用程序中进行if(of){}之后,它应该只是直接使用jojno而不是实际使用setcc创建一个整数0/1;通常这应该能够高效地内联。


自gcc7以来,有一种内建函数可以询问加法(在提升为给定类型之后)是否会溢出,而不返回值。

#include <stdbool.h>
int overflows(int a, int b) {
    bool of = __builtin_add_overflow_p(a, b, (int)0);
    return of;
}

使用gcc -O3编译 x86-64 System V 相关的代码,生成汇编语言,并将结果发布到 Godbolt 上。

overflows:
        xor     eax, eax
        add     edi, esi
        seto    al
        ret

请参见C/C++中检测有符号溢出


2

其他人已经提供了良好的替代代码以及为什么你尝试做的事情可能不会给出你想要的结果的原因,但是你代码中实际的错误在于你在没有弹出的情况下推入了堆栈状态。我会将汇编代码重写为:

pushf
pop %0

或者,如果你更喜欢低效的方式,你可以在汇编代码末尾添加add $4,%%esp来修复堆栈指针。


pushfd 在这里似乎是有序的?是的,我将进行一些研究,以了解如何阅读那个 GCC 语法;它看起来很难看,但可能有一些好的意义。 - nielsj
@nielsj:在32位模式下,pushf会被汇编为pushfd。不需要明确指定。如果您想要一个16位的push(在机器码中带有操作数大小前缀),则需要在任何非默认模式(32位或64位模式)中编写pushfw - Peter Cordes

0
以下的C程序在使用GCC编译时,将读取FLAGS寄存器,并且遵循整数返回到%eax寄存器的x86或x86_64机器的调用约定。您可能需要向编译器传递-zexecstack参数。
#include<stdio.h>
#include<stdlib.h>

int(*f)()=(void*)L"\xc3589c";

int main( int argc, char **argv ) {
  if( argc < 3 ) {
    printf( "Usage: %s <augend> <addend>\n", *argv );
    return 0;
  }
  int a=atoi(argv[1])+atoi(argv[2]);
  int b=f();
  printf("%d CF %d PF %d AF %d ZF %d SF %d TF %d IF %d DF %d OF %d IOPL %d NT %d RF %d VM %d AC %d VIF %d VIP %d ID %d\n", a, b&1, b/4&1, b>>4&1, b>>6&1, b>>7&1, b>>8&1, b>>9&1, b>>10&1, b>>11&1, b>>12&3, b>>14&1, b>>16&1, b>>17&1, b>>18&1, b>>19&1, b>>20&1, b>>21&1 );
}

在线试用!

这个看起来有趣的字符串字面量可以被分解为

0x0000000000000000:  9C    pushfq 
0x0000000000000001:  58    pop    rax
0x0000000000000002:  C3    ret    

为什么f是一个非const函数指针?这意味着调用者将不得不实际进行间接调用 [QWORD PTR f[rip]],而不能优化为 call .LC1。https://godbolt.org/z/5Eu-pS 此外,您没有对其进行原型保护(未知参数),而是使用了 f(void),因此x86-64 System V要求调用者设置AL=0。编译器通常使用 xor eax,eax 来执行此操作,从而破坏 add 的标志结果。 (巧合的是,gcc默认调整实际上在调用 f() 之前使用它来计算 +。ICC选择稍后计算 a,只需在 call .LC1 之后保存两个atoi结果即可。 - Peter Cordes
将机器代码放在自己的非内联函数中确实可以解决破坏红区的问题。但是,无论如何,如果禁用优化,编译器使用mov eax,0而不是xor eax,eax,你的代码恰好可以工作。通过我的更改,一些编译器仍然可以在启用优化的情况下适用于情况,但通常需要在内联汇编中进行添加操作才能确保安全。某些版本的某些编译器可能始终选择lea进行加法运算,例如使用-mtune=atom的gcc,当然,+也可以被优化为其他部分的一部分或在调用后执行。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接