很多相关问题< How is x86 instruction cache synchronized?>提到x86应该在自修改代码中正确处理i-cache同步。我编写了下面这段代码,它可以在与执行交错的不同线程上打开和关闭函数调用。我使用比较和交换操作作为额外的防护,以便修改是原子的。但我得到间歇性崩溃(SIGSEGV,SIGILL)并分析核心转储使我怀疑处理器是否正在尝试执行部分更新的指令。下面给出的代码和分析。可能是我在这里遗漏了一些东西。如果是这种情况,请告诉我。
toggle.c
#include <stdio.h>
#include <inttypes.h>
#include <time.h>
#include <pthread.h>
#include <sys/mman.h>
#include <errno.h>
#include <unistd.h>
int active = 1; // Whether the function is toggled on or off
uint8_t* funcAddr = 0; // Address where function call happens which we need to toggle on/off
uint64_t activeSequence = 0; // Byte sequence for toggling on the function CALL
uint64_t deactiveSequence = 0; // NOP byte sequence for toggling off the function CALL
inline int modify_page_permissions(uint8_t* addr) {
long page_size = sysconf(_SC_PAGESIZE);
int code = mprotect((void*)(addr - (((uint64_t)addr)%page_size)), page_size,
PROT_READ | PROT_WRITE | PROT_EXEC);
if (code) {
fprintf(stderr, "mprotect was not successfull! code %d\n", code);
fprintf(stderr, "errno value is : %d\n", errno);
return 0;
}
// If the 8 bytes we need to modify straddles a page boundary make the next page writable too
if (page_size - ((uint64_t)addr)%page_size < 8) {
code = mprotect((void*)(addr-((uint64_t)addr)%page_size+ page_size) , page_size,
PROT_READ | PROT_WRITE | PROT_EXEC);
if (code) {
fprintf(stderr, "mprotect was not successfull! code %d\n", code);
fprintf(stderr, "errno value is : %d\n", errno);
return 0;;
}
}
return 1;
}
void* add_call(void* param) {
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = 50000;
while (1) {
if (!active) {
if (activeSequence != 0) {
int status = modify_page_permissions(funcAddr);
if (!status) {
return 0;
}
uint8_t* start_addr = funcAddr - 8;
fprintf(stderr, "Activating foo..\n");
uint64_t res = __sync_val_compare_and_swap((uint64_t*) start_addr,
*((uint64_t*)start_addr), activeSequence);
active = 1;
} else {
fprintf(stderr, "Active sequence not initialized..\n");
}
}
nanosleep(&ts, NULL);
}
}
int remove_call(uint8_t* addr) {
if (active) {
// Remove gets called first before add so we initialize active and deactive state byte sequences during the first call the remove
if (deactiveSequence == 0) {
uint64_t sequence = *((uint64_t*)(addr-8));
uint64_t mask = 0x0000000000FFFFFF;
uint64_t deactive = (uint64_t) (sequence & mask);
mask = 0x9090909090000000; // We NOP 5 bytes of CALL instruction and leave rest of the 3 bytes as it is
activeSequence = sequence;
deactiveSequence = deactive | mask;
funcAddr = addr;
}
int status = modify_page_permissions(addr);
if (!status) {
return -1;
}
uint8_t* start_addr = addr - 8;
fprintf(stderr, "Deactivating foo..\n");
uint64_t res = __sync_val_compare_and_swap((uint64_t*)start_addr,
*((uint64_t*)start_addr), deactiveSequence);
active = 0;
// fprintf(stderr, "Result : %p\n", res);
}
}
int counter = 0;
void foo(int i) {
// Use the return address to determine where we need to patch foo CALL instruction (5 bytes)
uint64_t* addr = (uint64_t*)__builtin_extract_return_addr(__builtin_return_address(0));
fprintf(stderr, "Foo counter : %d\n", counter++);
remove_call((uint8_t*)addr);
}
// This thread periodically checks if the method is inactive and if so reactivates it
void spawn_add_call_thread() {
pthread_t tid;
pthread_create(&tid, NULL, add_call, (void*)NULL);
}
int main() {
spawn_add_call_thread();
int i=0;
for (i=0; i<1000000; i++) {
// fprintf(stderr, "i : %d..\n", i);
foo(i);
}
fprintf(stderr, "Final count : %d..\n\n\n", counter);
}
核心转储分析
Program terminated with signal 4, Illegal instruction.
#0 0x0000000000400a28 in main () at toggle.c:123
(gdb) info frame
Stack level 0, frame at 0x7fff7c8ee360:
rip = 0x400a28 in main (toggle.c:123); saved rip 0x310521ed5d
source language c.
Arglist at 0x7fff7c8ee350, args:
Locals at 0x7fff7c8ee350, Previous frame's sp is 0x7fff7c8ee360
Saved registers:
rbp at 0x7fff7c8ee350, rip at 0x7fff7c8ee358
(gdb) disas /r 0x400a28,+30
Dump of assembler code from 0x400a28 to 0x400a46:
=> 0x0000000000400a28 <main+64>: ff (bad)
0x0000000000400a29 <main+65>: ff (bad)
0x0000000000400a2a <main+66>: ff eb ljmpq *<internal disassembler error>
0x0000000000400a2c <main+68>: e7 48 out %eax,$0x48
(gdb) disas /r main
Dump of assembler code for function main:
0x00000000004009e8 <+0>: 55 push %rbp
...
0x0000000000400a24 <+60>: 89 c7 mov %eax,%edi
0x0000000000400a26 <+62>: e8 11 ff ff ff callq 0x40093c <foo>
0x0000000000400a2b <+67>: eb e7 jmp 0x400a14 <main+44>
可以看出,指令指针似乎位于CALL指令内的地址内,处理器显然正在尝试执行该错位指令,导致非法指令错误。
答案 0 :(得分:3)
在80x86上,大多数调用使用相对位移,而不是绝对地址。基本上它的&#34; 在这里调用代码+&lt; displacement &gt;&#34; 而不是&#34; 调用&lt; 地址&gt; &#34;。
对于64位代码,位移可以是8位或32位。它永远不会是64位。
例如,对于具有8位位移的2字节&#34;调用&#34;在调用指令,call
操作码本身和指令的操作数(位移)之前,您将丢失6个字节。
另一个例子,对于一个5字节&#34;调用32位位移&#34;在调用指令,call
操作码本身和指令的操作数(位移)之前,你要删除3个字节。
...然而
这些不是唯一的通话方式。例如,您可以使用函数指针调用,其中被调用的代码的地址根本不在指令中(但可以在寄存器中或在内存中是变量)。还有一种称为&#34;尾调用优化的优化&#34;其中call
后跟ret
被替换为jmp
(可能还有一些额外的堆栈用于传递参数,清理调用者的局部变量等)。< / p>
本质;你的代码严重破坏,你无法覆盖所有可能的极端情况,你不应该这样做,你可能应该使用函数指针而不是自修改代码(这将是更快,更轻松,更便携。)
答案 1 :(得分:3)
我认为你的问题是你用5个1字节的NOP替换了一个5字节的CALL指令。考虑当你的线程执行了3个NOP时会发生什么,然后你的主线程决定重新交换CALL指令。你的线程的PC将在CALL指令的中间三个字节,因此将执行一个意外和可能的非法指令。
您需要做的是将5字节CALL指令与5字节NOP交换。您只需要找到一个不执行任何操作的多字节指令(例如,或者对自己注册一个寄存器),如果您需要一些额外的字节,请预先添加一些前缀字节,例如gs覆盖前缀和地址大小覆盖前缀(两者都无济于事。通过使用5字节的NOP,您的线程将保证在CALL指令处或通过CALL指令,但从不在其内部。