C 使用 NASM 指向 EFLAGS 的指针
C Pointer to EFLAGS using NASM
为了我学校的一项任务,我需要编写一个 C 程序,该程序使用汇编程序执行 16 位加法。除了结果外,还应返回 EFLAGS。
这是我的 C 程序:
int add(int a, int b, unsigned short* flags);//function must be used this way
int main(void){
unsigned short* flags = NULL;
printf("%d\n",add(30000, 36000, flags);// printing just the result
return 0;
}
目前程序只打印结果而不是标志,因为我无法获得它们。
这是我使用的汇编程序 NASM:
global add
section .text
add:
push ebp
mov ebp,esp
mov ax,[ebp+8]
mov bx,[ebp+12]
add ax,bx
mov esp,ebp
pop ebp
ret
现在一切顺利。但我不知道如何获取必须在 [ebp+16]
处指向标志寄存器的指针。教授说我们将不得不使用pushfd
命令。
我的问题就出在汇编代码上。得到flags的解决方案后,我会修改C程序来给出flags。
这只是一个简单的方法。我没有测试过,但你应该明白了......
只需将 ESP
设置为指针值,将其递增 2(即使对于 32 位 arch)和 PUSHF
,如下所示:
global add
section .text
add:
push ebp
mov ebp,esp
mov ax,[ebp+8]
mov bx,[ebp+12]
add ax,bx
; --- here comes the mod
mov esp, [ebp+16] ; this will set ESP to the pointers address "unsigned short* flags"
lea esp, [esp+2] ; adjust ESP to address above target
db 66h ; operand size prefix for 16-bit PUSHF (alternatively 'db 0x66', depending on your assembler
pushf ; this will save the lower 16-bits of EFLAGS to WORD PTR [EBP+16] = [ESP+2-2]
; --- here ends the mod
mov esp,ebp
pop ebp
ret
这应该可行,因为 PUSHF 将 ESP
减 2,然后将值保存到 WORD PTR [ESP]
。因此必须在使用指针地址之前增加它。将 ESP
设置为适当的值可以让您命名 PUSHF
的直接目标。
通常情况下,您只需使用调试器查看标志,而不是编写所有代码以将它们放入 C 变量中以进行调试打印。特别是因为体面的调试器会为您象征性地解码条件标志,而不是显示十六进制值。
您不必知道或关心 FLAGS 中的哪个位是 CF,哪个位是 ZF。 (这些信息也与编写实际程序无关。我没有记住它,我只是知道哪些标志是通过不同的条件测试的 jae
or jl
。当然,最好理解 FLAGS 只是您可以复制的数据,save/restore,甚至可以根据需要进行修改)
您的函数参数和 return 值是 int
,在您使用的 System V 32 位 x86 ABI 中是 32 位的。 (链接到 x86 标签 wiki 中的 ABI 文档)。编写一个只查看其输入的低 16 位并在输出的高 16 位中留下高位垃圾的函数是一个错误。原型中的 int
return 值告诉编译器 EAX 的所有 32 位都是 return 值的一部分。
正如 Michael 所指出的,您似乎是在说您的作业需要使用 16 位 ADD。与查看完整的 32 位相比,这将产生具有不同输入的进位、溢出和其他条件。 (顺便说一句,this article explains carry vs. overflow very well。)
这就是我要做的。注意 ADD 的 32 位操作数大小。
global add
section .text
add:
push ebp
mov ebp,esp ; stack frames are optional, you can address things relative to ESP
mov eax, [ebp+8] ; first arg: No need to avoid loading the full 32 bits; the next insn doesn't care about the high garbage.
add ax, [ebp+12] ; low 16 bits of second arg. Operand-size implied by AX
cwde ; sign-extend AX into EAX
mov ecx, [ebp+16] ; the pointer arg
pushf ; the simple straightforward way
pop edx
mov [ecx], dx ; Store the low 16 of what we popped. Writing word [ecx] is optional, because dx implies 16-bit operand-size
; be careful not to do a 32-bit store here, because that would write outside the caller's object.
; mov esp,ebp ; redundant: ESP is still pointing at the place we pushed EBP, since the push is balanced by an equal-size pop
pop ebp
ret
CWDE (the 16->32 form of the 8086 CBW instruction) is not to be confused with CWD(AX -> DX:AX 8086 指令)。如果您不使用 AX,那么 MOVSX / MOVZX 是执行此操作的好方法。
有趣的方式:我们可以直接向目标内存地址执行 16 位弹出,而不是使用默认操作数大小并进行 32 位入栈和出栈。这将使堆栈不平衡,因此我们可以再次取消注释 mov esp, ebp
,或使用 16 位 pushf(带有操作数大小前缀,according to the docs 使其仅压入低 16 FLAGS,不是 32 位 EFLAGS。)
; What I'd *really* do: maximum efficiency if I had to use the 32-bit ABI with args on the stack, instead of args in registers
global add
section .text
add:
mov eax, [esp+4] ; first arg, first thing above the return address
add ax, [esp+8] ; second arg
cwde ; sign-extend AX into EAX
mov ecx, [esp+12] ; the pointer
pushfw ; push the low 16 of FLAGS
pop word [ecx] ; pop into memory pointed to by unsigned short* flags
ret
PUSHFW 和 POP WORD 都将 assemble 带有操作数大小前缀。 objdump -Mintel
的输出,它使用与 NASM:
略有不同的语法
4000c0: 66 9c pushfw
4000c2: 66 8f 01 pop WORD PTR [ecx]
PUSHFW 与 o16 PUSHF
相同。在 NASM 中,o16
应用操作数大小前缀。
如果您只需要低 8 位标志(不包括 OF),您可以使用 LAHF 将 FLAGS 加载到 AH 中并存储它。
我不推荐直接推入目的地。暂时将堆栈指向某个随机地址通常是不安全的。具有信号处理程序的程序将异步使用堆栈下方的 space。这就是为什么在将它与 sub esp, 32
或其他任何东西一起使用之前必须保留堆栈 space 的原因,即使你不打算进行会通过将更多内容压入堆栈来覆盖它的函数调用。唯一的例外是当您有 red-zone.
您的 C 来电:
您正在传递 NULL 指针,因此您的 asm 函数当然会出现段错误。传递本地地址以将函数存储到某处。
int add(int a, int b, unsigned short* flags);
int main(void) {
unsigned short flags;
int result = add(30000, 36000, &flags);
printf("%d %#hx\n", result, flags);
return 0;
}
为了我学校的一项任务,我需要编写一个 C 程序,该程序使用汇编程序执行 16 位加法。除了结果外,还应返回 EFLAGS。
这是我的 C 程序:
int add(int a, int b, unsigned short* flags);//function must be used this way
int main(void){
unsigned short* flags = NULL;
printf("%d\n",add(30000, 36000, flags);// printing just the result
return 0;
}
目前程序只打印结果而不是标志,因为我无法获得它们。
这是我使用的汇编程序 NASM:
global add
section .text
add:
push ebp
mov ebp,esp
mov ax,[ebp+8]
mov bx,[ebp+12]
add ax,bx
mov esp,ebp
pop ebp
ret
现在一切顺利。但我不知道如何获取必须在 [ebp+16]
处指向标志寄存器的指针。教授说我们将不得不使用pushfd
命令。
我的问题就出在汇编代码上。得到flags的解决方案后,我会修改C程序来给出flags。
这只是一个简单的方法。我没有测试过,但你应该明白了......
只需将 ESP
设置为指针值,将其递增 2(即使对于 32 位 arch)和 PUSHF
,如下所示:
global add
section .text
add:
push ebp
mov ebp,esp
mov ax,[ebp+8]
mov bx,[ebp+12]
add ax,bx
; --- here comes the mod
mov esp, [ebp+16] ; this will set ESP to the pointers address "unsigned short* flags"
lea esp, [esp+2] ; adjust ESP to address above target
db 66h ; operand size prefix for 16-bit PUSHF (alternatively 'db 0x66', depending on your assembler
pushf ; this will save the lower 16-bits of EFLAGS to WORD PTR [EBP+16] = [ESP+2-2]
; --- here ends the mod
mov esp,ebp
pop ebp
ret
这应该可行,因为 PUSHF 将 ESP
减 2,然后将值保存到 WORD PTR [ESP]
。因此必须在使用指针地址之前增加它。将 ESP
设置为适当的值可以让您命名 PUSHF
的直接目标。
通常情况下,您只需使用调试器查看标志,而不是编写所有代码以将它们放入 C 变量中以进行调试打印。特别是因为体面的调试器会为您象征性地解码条件标志,而不是显示十六进制值。
您不必知道或关心 FLAGS 中的哪个位是 CF,哪个位是 ZF。 (这些信息也与编写实际程序无关。我没有记住它,我只是知道哪些标志是通过不同的条件测试的 jae
or jl
。当然,最好理解 FLAGS 只是您可以复制的数据,save/restore,甚至可以根据需要进行修改)
您的函数参数和 return 值是 int
,在您使用的 System V 32 位 x86 ABI 中是 32 位的。 (链接到 x86 标签 wiki 中的 ABI 文档)。编写一个只查看其输入的低 16 位并在输出的高 16 位中留下高位垃圾的函数是一个错误。原型中的 int
return 值告诉编译器 EAX 的所有 32 位都是 return 值的一部分。
正如 Michael 所指出的,您似乎是在说您的作业需要使用 16 位 ADD。与查看完整的 32 位相比,这将产生具有不同输入的进位、溢出和其他条件。 (顺便说一句,this article explains carry vs. overflow very well。)
这就是我要做的。注意 ADD 的 32 位操作数大小。
global add
section .text
add:
push ebp
mov ebp,esp ; stack frames are optional, you can address things relative to ESP
mov eax, [ebp+8] ; first arg: No need to avoid loading the full 32 bits; the next insn doesn't care about the high garbage.
add ax, [ebp+12] ; low 16 bits of second arg. Operand-size implied by AX
cwde ; sign-extend AX into EAX
mov ecx, [ebp+16] ; the pointer arg
pushf ; the simple straightforward way
pop edx
mov [ecx], dx ; Store the low 16 of what we popped. Writing word [ecx] is optional, because dx implies 16-bit operand-size
; be careful not to do a 32-bit store here, because that would write outside the caller's object.
; mov esp,ebp ; redundant: ESP is still pointing at the place we pushed EBP, since the push is balanced by an equal-size pop
pop ebp
ret
CWDE (the 16->32 form of the 8086 CBW instruction) is not to be confused with CWD(AX -> DX:AX 8086 指令)。如果您不使用 AX,那么 MOVSX / MOVZX 是执行此操作的好方法。
有趣的方式:我们可以直接向目标内存地址执行 16 位弹出,而不是使用默认操作数大小并进行 32 位入栈和出栈。这将使堆栈不平衡,因此我们可以再次取消注释 mov esp, ebp
,或使用 16 位 pushf(带有操作数大小前缀,according to the docs 使其仅压入低 16 FLAGS,不是 32 位 EFLAGS。)
; What I'd *really* do: maximum efficiency if I had to use the 32-bit ABI with args on the stack, instead of args in registers
global add
section .text
add:
mov eax, [esp+4] ; first arg, first thing above the return address
add ax, [esp+8] ; second arg
cwde ; sign-extend AX into EAX
mov ecx, [esp+12] ; the pointer
pushfw ; push the low 16 of FLAGS
pop word [ecx] ; pop into memory pointed to by unsigned short* flags
ret
PUSHFW 和 POP WORD 都将 assemble 带有操作数大小前缀。 objdump -Mintel
的输出,它使用与 NASM:
4000c0: 66 9c pushfw
4000c2: 66 8f 01 pop WORD PTR [ecx]
PUSHFW 与 o16 PUSHF
相同。在 NASM 中,o16
应用操作数大小前缀。
如果您只需要低 8 位标志(不包括 OF),您可以使用 LAHF 将 FLAGS 加载到 AH 中并存储它。
我不推荐直接推入目的地。暂时将堆栈指向某个随机地址通常是不安全的。具有信号处理程序的程序将异步使用堆栈下方的 space。这就是为什么在将它与 sub esp, 32
或其他任何东西一起使用之前必须保留堆栈 space 的原因,即使你不打算进行会通过将更多内容压入堆栈来覆盖它的函数调用。唯一的例外是当您有 red-zone.
您的 C 来电:
您正在传递 NULL 指针,因此您的 asm 函数当然会出现段错误。传递本地地址以将函数存储到某处。
int add(int a, int b, unsigned short* flags);
int main(void) {
unsigned short flags;
int result = add(30000, 36000, &flags);
printf("%d %#hx\n", result, flags);
return 0;
}