스택큐힙리스트

x86에서 자가 수정 코드(self-modifying code)와 함께 발생하는 오래된 명령어 가져오기 관찰하기 본문

카테고리 없음

x86에서 자가 수정 코드(self-modifying code)와 함께 발생하는 오래된 명령어 가져오기 관찰하기

스택큐힙리스트 2023. 8. 29. 17:51
반응형

 

인텔의 메뉴얼에서 제시된 대로, 메모리에 명령을 작성할 수 있는 것으로 알고 있는데, 명령 미리 읽기 큐는 이미 오래된 명령을 미리 가져와서 해당 이전 명령을 실행하게 될 것입니다. 그러나 나는 이러한 동작을 관찰하는 데 실패했습니다. 나의 방법론은 다음과 같습니다.

인텔 소프트웨어 개발 매뉴얼 11.6 절에 따르면,

 

현재 프로세서에서 캐시된 코드 세그먼트에 대한 메모리 위치 쓰기는 연관된 캐시 라인(또는 라인들)을 무효화시킵니다. 이 체크는 명령어의 물리적 주소를 기반으로 합니다. 더불어 P6 패밀리와 펜티엄 프로세서는 코드 세그먼트에 대한 쓰기가 실행을 위해 미리 가져온 명령어를 수정할 수 있는지 여부도 체크합니다. 쓰기가 미리 가져온 명령어에 영향을 미치면 프리페치 큐가 무효화됩니다. 이후의 체크는 명령어의 선형 주소를 기반으로 합니다.

 

그래서, 만약 낡은 명령을 실행하기를 원한다면, 동일한 물리 페이지를 가리키는 두 개의 다른 선형 주소가 필요해 보입니다. 따라서, 파일을 두 개의 다른 주소에 메모리에 맵핑합니다.

#@!'int fd = open("code_area", O_RDWR | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO);

assert(fd>=0);

write(fd, zeros, 0x1000);

uint8_t *a1 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,

MAP_FILE | MAP_SHARED, fd, 0);

uint8_t *a2 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,

MAP_FILE | MAP_SHARED, fd, 0);

assert(a1 != a2);

'#@!

저는 포인터 매개변수로 받는 어셈블리 함수가 있습니다. 해당 함수는 변경하고자 하는 명령어의 포인터를 인자로 받습니다.

#@!'fun:

push %rbp

mov %rsp, %rbp

 

xorq %rax, %rax # Return value 0

 

# A far jump simulated with a far return

# Push the current code segment %cs, then the address we want to far jump to

 

xorq %rsi, %rsi

mov %cs, %rsi

pushq %rsi

leaq copy(%rip), %r15

pushq %r15

lretq

 

copy:

# Overwrite the two nops below with `inc %eax'. We will notice the change if the

# return value is 1, not zero. The passed in pointer at %rdi points to the same physical

# memory location of fun_ins, but the linear addresses will be different.

movw $0xc0ff, (%rdi)

 

fun_ins:

nop # Two NOPs gives enough space for the inc %eax (opcode FF C0)

nop

pop %rbp

ret

fun_end:

nop

'#@!

C 언어에서, 내가 코드를 메모리 맵 파일에 복사합니다. 나는 !@##@!'a1'!@##@! 선형 주소에서 함수를 호출하지만, 코드 수정의 대상으로 !@##@!'a2'!@##@! 포인터를 전달합니다.

#@!'#define DIFF(a, b) ((long)(b) - (long)(a))

long sz = DIFF(fun, fun_end);

memcpy(a1, fun, sz);

void *tochange = DIFF(fun, fun_ins);

int val = ((int (*)(void*))a1)(tochange);

'#@!

만약 CPU가 수정된 코드를 탐지했다면, val==1입니다. 그렇지 않다면, 만약 잘못된 명령이 실행되었다면 (두 개의 nops), val==0입니다.

나는 이 작업을 1.7GHz 인텔 코어 i5(2011 맥북 에어)와 인텔(R) 제온(R) CPU X3460 @ 2.80GHz에서 실행했다. 하지만 매번 val == 1을 보면 CPU가 항상 새로운 명령을 인지한다는 것을 알 수 있다.

내가 관찰하려는 행동에 대한 경험이 있는 사람이 있나요? 내 추론이 맞나요? 제 코어 i5 프로세서에 대한 매뉴얼에서 P6와 펜티엄 프로세서가 언급되는 것에 대해 조금 헷갈립니다. 아마도 CPU가 명령어 미리 로드 큐를 플러시하는 원인은 다른 것이 있는 걸까요? 어떠한 통찰력이라도 큰 도움이 될 것입니다!

 

답변 1

 

나는, 당신이 CPU의 성능 카운터 (당신의 에어 파워북에 사용되는 Sandy Bridge에 있는 것; 그리고 Nehalem에 있는 당신의 Xeon에도 있는 'MACHINE_CLEARS' 이벤트의 일부)를 확인해보아야 한다고 생각해요. 그 값은 "smc"를 검색해보세요. 이 값을 찾기 위해 'oprofile' , 'perf' 또는 Intel의 Vtune 을 사용할 수 있습니다.

http://software.intel.com/sites/products/documentation/doclib/iss/2013/amplifier/lin/ug_docs/GUID-F0FD7660-58B5-4B5D-AA9A-E1AF21DDCA0E.html

 

기계를 청소합니다.

메트릭 설명

일부 이벤트는 전체 파이프라인을 지우고 마지막으로 폐기된 명령어 이후에서부터 다시 시작해야합니다. 이 지표는 세 가지 이벤트를 측정합니다: 메모리 순서 위반, 자기 수정 코드 및 잘못된 주소 범위로의 특정 로드.

가능한 문제들

실행 시간의 상당 부분은 기계 클리어를 처리하는 데 사용됩니다. 특정 원인을 확인하기 위해 MACHINE_CLEARS 이벤트를 검토하세요.

 

SMC: 이거 왜 이러지!

 

MACHINE_CLEARS 이벤트 코드: 0xC3

SMC 마스크: 0x04

자가 수정 코드 (SMC)가 감지되었습니다.

자가 수정 코드 기계 초기화 감지 횟수.

 

인텔은 또한 http://software.intel.com/en-us/forums/topic/345561 에 대해서도 언급하고 있으며 (이와 관련된 링크 Intel Performance Bottleneck Analyzer's taxonomy 에서 확인할 수 있습니다.

 

이 이벤트는 자가 수정 코드가 감지될 때 발생합니다. 일반적으로 이 이벤트는 이진 편집을 수행하여 특정 경로를 취하도록 강제하는 사용자들에 의해 사용될 수 있습니다(예: 해커들). 이 이벤트는 프로그램이 코드 섹션에 쓰는 횟수를 세어줍니다. 자가 수정 코드는 모든 Intel 64 및 IA-32 프로세서에서 심각한 벌칙을 유발합니다. 수정된 캐시 라인은 L2 및 LLC 캐시에 다시 기록됩니다. 또한, 명령어들은 다시 로드되어 성능에 벌칙을 유발시킵니다.

 

나는 당신이 그러한 이벤트 몇 가지를 볼 것이라고 생각합니다. 그렇다면 CPU는 코드의 자체 수정 행위를 감지하고 "머신 클리어"를 발생시켜 파이프라인을 완전히 재시작할 수 있었습니다. 첫 번째 단계는 Fetch이며, 그들은 새로운 옵코드를 위해 L2 캐시에 요청할 것입니다. 나는 당신의 코드 실행 당 SMC 이벤트의 정확한 횟수에 매우 관심이 있습니다 - 이는 지연 시간에 대한 어느 정도의 추정치를 제공할 것입니다. (SMC는 1 유닛이 1.5 CPU 사이클로 가정되는 일부 단위로 계산됩니다 - 인텔 최적화 매뉴얼 B.6.2.6 참조)

우리는 인텔이 "마지막 은퇴된 명령어 바로 다음부터 재시작됨"이라고 말하고 있으니까, 나는 마지막 은퇴된 명령어는 mov 이 될 것이라고 생각하고, 당신의 nops는 이미 파이프라인 안에 있습니다. 그러나 mov의 은퇴 시점에서 SMC가 발생하여 nops를 포함한 파이프라인 안의 모든 것을 제거할 것입니다.

이 SMC 유발 파이프라인 재시작은 저렴하지 않습니다. Agner은 "$#*^#@!$&#@! - '17.10 자기 수정 코드 (모든 프로세서)'(나는 Core2/CoreiX 가 여기서 PM과 비슷하다고 생각합니다)" 에서 일부 측정치를 가지고 있습니다.

 

코드를 수정한 직후에 코드를 실행하는 경우 P1의 경우 약 19 클럭, PMMX의 경우 31 클럭, PPro, P2, P3, PM의 경우 150-300 클럭의 패널티가 발생합니다. P4는 자가 수정 코드 후에 트레이스 캐시 전체를 제거합니다. 80486 및 이전 프로세서는 코드 캐시를 비우기 위해 수정 및 수정된 코드 사이에서 점프가 필요합니다.

자기 수정 코드는 좋은 프로그래밍 관행으로 여겨지지 않습니다. 속도 향상이 상당하고 수정된 코드가 사용될 때 많은 횟수로 실행되어 이점이 자기 수정 코드 사용에 따른 단점을 상쇄시키는 경우에만 사용되어야 합니다.

 

여기서 SMC 탐지기가 실패하도록 서로 다른 선형 주소를 사용하는 것을 권장했습니다:

x86 명령어 캐시는 어떻게 동기화되나요? - 실제 인텔 문서를 찾아보겠습니다... 현재 실제 질문에 대한 답변을 드릴 수 없습니다.

여기에 일부 힌트가 있을 수 있습니다: Optimization manual, 248966-026, April 2012' "3.6.9 코드와 데이터의 혼합":

 

쓰기 가능한 데이터를 코드 세그먼트에 배치하는 것은 자기 수정 코드와 구별하기 어려울 수 있습니다. 코드 세그먼트에 있는 쓰기 가능한 데이터는 자기 수정 코드와 동일한 성능 저하를 겪을 수 있습니다.

 

그리고 다음 섹션

 

소프트웨어는 실행 중인 1-KByte 하위 페이지와 동일한 코드 페이지에 쓰지 않거나, 쓰는 동안에는 2-KByte 하위 페이지에서 코드를 가져 오지 않아야합니다. 또한 직접 또는 추론적으로 실행 된 코드를 데이터 페이지로 다른 프로세서와 공유하는 경우에는 SMC(Secure Mode Change) 조건을 유발하여 기계 및 trace cache의 전체 파이프라인이 지워질 수 있습니다. 이는 자기 수정 코드 조건에 의한 것입니다.

 

그래서, 쓰거나 실행 가능한 하위 페이지의 교차점을 제어하는 일부 도표가 가능한 것 같습니다.

당신은 다른 스레드에서 수정을 시도할 수 있습니다 (코드의 교차 수정) - 하지만 매우 조심스럽게 스레드 동기화와 파이프라인 플러싱이 필요합니다 (작성자 스레드에서 지연을 무작위로 강제하는 것도 포함시킬 수 있습니다; 동기화 직후에 CPUID를 원하는 대로 실행하세요). 그러나 이미 "핵"을 사용하여 이 문제를 해결했다는 사실을 알고 계셔야 합니다 - 'US6857064'특허를 확인하세요.

 

제가 좀 헷갈리는 것은 매뉴얼에 P6 및 펜티엄 프로세서에 대한 언급이 있는 것입니다.

 

이것은 가능합니다만, 인텔의 명령 설명서의 오래된 버전을 검색하여 디코딩하고 실행한 경우에만 가능합니다. 파이프라인을 재설정하고이 버전을 확인할 수 있습니다: 'Order Number: 325462-047US, June 2013' "11.6 자가 수정 코드". 이 버전은 아직 새로운 CPU에 대해 어떤 내용도 언급하지 않지만, 다른 가상 주소를 사용하여 수정하는 경우, 마이크로 아키텍처 간에 동작이 호환되지 않을 수 있습니다 (네해렘/샌디 브리지에서 작동할 수 있지만 스카이먼트에서는 작동하지 않을 수 있음).

 

11.6 자기 수정 코드

 

프로세서에서 캐시된 코드 세그먼트의 메모리 위치에 대한 쓰기는 관련된 캐시 라인(또는 라인들)이 무효화되도록 합니다. 이 체크는 명령어의 물리적 주소를 기준으로 합니다. 게다가 P6 패밀리와 펜티엄 프로세서는 코드 세그먼트에 대한 쓰기가 미리 실행을 위해 프리패치된 명령어를 수정할 수 있는지를 확인합니다. 쓰기가 프리패치된 명령어에 영향을 미친다면, 프리패치 큐가 무효화됩니다. 이후의 체크는 명령어의 선형 주소를 기반으로 합니다. 펜티엄 4 및 인텔 제온 프로세서의 경우, 이미 디코드되어 추적 캐시에 상주하는 대상 명령어의 코드 세그먼트 내 명령어에 대한 쓰기 또는 스눕은 전체 추적 캐시를 무효화합니다. 이후의 동작은 코드를 자기 수정하는 프로그램이 펜티엄 4 및 인텔 제온 프로세서에서 실행될 때 성능의 심각한 저하를 야기할 수 있다는 것을 의미합니다.

실제로는, 선형 주소의 확인은 IA-32 프로세서들 간에 호환성 문제를 일으키지 않아야 합니다. 자기 수정 코드를 포함한 응용 프로그램은 수정 및 명령어 검색을 위해 동일한 선형 주소를 사용합니다.

시스템 소프트웨어, 예를 들어 디버거처럼, 명령어를 가져올 때와 다른 선형 주소를 사용하여 명령어를 수정할 수 있다면, 수정된 명령어가 실행되기 전에 일련화 작업(예: CPUID 명령어)을 실행하여 자동으로 명령어 캐시와 프리페치 큐를 다시 동기화합니다. (셀프 수정 코드의 사용에 대한 자세한 내용은 8.1.3 섹션인 "셀프 수정 및 상호 수정 코드 처리"를 참조하십시오.)

인텔486 프로세서의 경우, 캐시에 있는 명령에 대한 쓰기는 캐시와 메모리 모두에서 수정됩니다. 그러나 쓰기 전에 명령이 사전 패치됐다면, 이전 버전의 명령이 실행될 수 있습니다. 이전 버전의 명령이 실행되는 것을 방지하기 위해, 명령을 수정하는 쓰기 다음에 바로 점프 명령을 작성하여 명령 사전 패치 유닛을 플러시하세요.

 

실제 업데이트, "SMC 감지"를 구글에 검색하였습니다(따옴표 포함) 그리고 현대의 Core2/Core iX가 SMC를 감지하는 방법에 관한 몇 가지 세부 정보와 Xeon과 Pentium의 SMC 감지기에서 걸리는 여러 오류 목록이 있습니다.

 

'http://www.google.com/patents/US6237088' 파이프라인에서 비행 중 지시사항을 추적하는 시스템 및 방법 @ 2001

DOI 10.1535/itj.1203.03 (구글에서 찾아보세요, citeseerx.ist.psu.edu 에 무료 버전이 있습니다) - "포함 필터"가 Penryn에 추가되어 잘못된 SMC 검출 수를 줄이는데 사용되었습니다. "기존 포함 검출 메커니즘"은 그림 9에 나와 있습니다.

'http://www.google.com/patents/US6405307' - SMC 감지 로직에 대한 이전 특허

 

특허 US6237088 (FIG5, 요약)에 따르면 "라인 주소 버퍼"가 있습니다 (하나의 주소당 한 개의 명령을 가져온다는 의미로, 캐시 라인 정확도로 가져온 IP가 가득 찬 버퍼). 모든 저장 또는 더 정확히 말하면 모든 저장 주소 단계는 현재 실행 중인 명령 중 어느 것과 겹치는 지를 확인하기 위해 병렬 비교기로 입력될 것입니다.

이 두 개의 특허는 명확히 말하지 않습니다. SMC 로직에서 물리 주소나 논리 주소를 사용할 것인지... Sandy Bridge의 L1i는 VIPT입니다 ( !@##@!'Virtually indexed, physically tagged'!@##@! , 인덱스에 대한 가상 주소 및 태그에 대한 물리 주소). )에 따라서 !@##@!'http://nick-black.com/dankwiki/index.php/Sandy_Bridge'!@##@! 이므로 L1 캐시가 데이터를 반환할 때 물리 주소를 가지고 있습니다. 저는 인텔이 SMC 검출 로직에서 물리 주소를 사용할 수도 있다고 생각합니다.

더욱이, 1999년부터 (출판은 2003년에 이루어졌으며, CPU 설계 주기는 약 3-5년이라는 점을 기억하세요) "요약" 섹션에서는 SMC가 이제 TLB에 있고 물리적 주소를 사용한다고 말합니다 (또는 다른 말로 - SMC 탐지기를 속이지 말아주세요).

 

자체 수정 코드는 번역 탐색 버퍼를 사용하여 감지됩니다. 이 버퍼에는 스토어의 물리 메모리 주소를 통해 수행할 수 있는 스눕이 저장됩니다. ... 페이지 주소보다 더 세부적인 정밀도를 제공하기 위해 캐시의 각 항목에는 메모리 내의 페이지 일부에 대한 정보를 연관시키는 세부 일치 비트가 포함됩니다.

 

(페이지 일부, 특허 US6594734에서 quadrant로 참조되는 일부분, 1K 하위페이지 처럼 들리지 않나요?)

그럼 그들이 말합니다.

 

따라서 스눕스는 메모리로의 저장 명령에 의해 트리거되어 명령 캐시에 저장된 모든 명령의 물리적 주소를 연관된 페이지 또는 페이지의 모든 명령의 주소와 비교함으로써 SMC 탐지를 수행할 수 있습니다. 주소 일치가 발생하면 메모리 위치가 수정된 것을 나타냅니다. 주소 일치가 발생한 경우 SMC 상태를 나타내며 퇴직 유닛에 의해 명령 캐시와 명령 파이프라인이 플러시되고 새로운 명령이 메모리에서 검색되어 명령 캐시에 저장됩니다.

SMC 감지를 위한 스눕은 물리적인 것이기 때문에, ITLB는 일반적으로 선형 주소를 물리적 주소로 변환하기 위한 입력으로 사용됩니다. 그러므로 ITLB는 물리적 주소를 위한 내용-주소 메모리로 구성되며, 추가적인 입력 비교 포트 (스눕 포트 또는 역변환 포트로 불리는)도 포함하고 있습니다.

 

- 그래서 SMC를 검출하기 위해, 상점들에게 피지컬 어드레스를 인스트럭션 버퍼를 통해 다시 전달하도록 스눕을 강제로 실행합니다 (다른 코어/씨피유 또는 캐시에 대한 DMA 쓰기에서 비슷한 스눕이 전달될 수도 있습니다....). 만약 스눕의 피지컬 어드레스가 인스트럭션 버퍼에 저장된 캐시 라인과 충돌한다면, iTLB로부터 리타이어먼트 유닛으로 전달되는 SMC 신호를 통해 파이프라인을 재시작할 것입니다. dTLB를 통한 iTLB를 거쳐 리타이어먼트 유닛까지 이러한 스눕 루프가 얼마나 많은 CPU 클럭을 낭비시킬지 상상할 수 있을 것입니다 (mov보다 일찍 실행되었지만 부작용이 없는 다음"nop" 명령어를 리타이어할 수 없습니다). 그렇지만 뭐야? ITLB에는 피지컬 어드레스 입력과 미친 동적인 코드에 대응하고 방어하기 위한 두 번째 CAM(큰 크고 뜨거운)도 있습니다.

PS: 그리고 만약 우리가 매우 큰 페이지(4M이나 1G)와 함께 작업한다면 어떨까요? L1TLB에는 매우 큰 페이지 항목이 있으며 4MB 페이지의 1/4에 대해 많은 잘못된 SMC 감지가 있을 수 있습니다...

PPS: 변형이 있습니다. SMC의 잘못된 처리가 다른 선형 주소로 한정되었던 초기 P6/Ppro/P2에만 존재했습니다...

 

답변 2

 

자체 수정 코드로 인해 x86에서 사용되는 구식 명령 검색 관찰하기

 

이 글은 x86 아키텍처에서 발생하는 구식 명령 검색에 대해 기술적으로 설명하고 있습니다. 구식 명령 검색은 처리 능력과 성능에 영향을 미칠 수 있는 문제 중 하나입니다. 특히, 자체 수정 코드에 의해 호출되는 경우에는 더욱 복잡해질 수 있습니다.

 

먼저, 구식 명령 검색에 대해 이해해야 합니다. x86 프로세서에서 명령은 프로그램 카운터(PC)에 저장된 주소로부터 로드됩니다. 일반적으로, CPU는 명령 집합 내의 다음 명령을 순차적으로 가져옵니다. 그러나 자체 수정 코드가 사용되는 경우, 프로그램이 실행되는 동안 메모리 내의 명령들이 동적으로 수정될 수 있습니다. 이로 인해 명령 검색이 복잡해지고 부적절한 결과를 초래할 수 있습니다.

 

자체 수정 코드는 주로 프로그램의 효율성을 향상시키기 위해 사용됩니다. 예를 들어, 실행 시점에 일부 프로그램 섹션을 수정하여 로드되는 명령을 조정하거나 프로그램 흐름을 최적화할 수 있습니다. 그러나 이러한 수정은 명령 검색 과정에서 예기치 않은 결과를 초래할 수 있습니다.

 

구식 명령 검색은 CPU의 명령 캐시와 밀접한 관련이 있습니다. 명령 캐시는 CPU의 처리 속도를 향상시키기 위해 사용되는 메모리 구조입니다. 일반적으로, x86 프로세서는 빠른 명령 캐시에 의존하여 명령을 빠르게 검색합니다. 그러나 자체 수정 코드는 명령을 수정하기 때문에 명령 캐시의 일관성을 깨뜨릴 수 있습니다. 이는 CPU가 새로운 명령을 올바르게 검색하지 못하거나 이미 수정된 명령을 가져오는 등의 문제를 발생시킬 수 있습니다.

 

자체 수정 코드와 함께 구식 명령 검색을 해결하는 방법 중 하나는 CPU의 명령 캐시 무효화입니다. 명령 캐시를 무효화하면 CPU는 캐시된 명령을 무시하고 메모리에서 명령을 다시 검색합니다. 이를 통해 새로운 명령이나 수정 사항이 즉시 적용될 수 있습니다. 그러나 명령 캐시 무효화는 CPU의 성능에 악영향을 미칠 수 있기 때문에 신중하게 사용해야 합니다.

 

마지막으로, 구식 명령 검색을 최소화하기 위해 자체 수정 코드 사용을 피하는 것이 좋습니다. 대부분의 상황에서 자체 수정은 필요하지 않으며, 코드의 복잡성과 예측 불가능한 결과를 초래할 수 있습니다. 따라서 명령 검색의 효율성과 일관성을 유지하기 위해 자체 수정을 피하는 것이 필요합니다.

 

요약하면, x86 아키텍처에서 구식 명령 검색은 자체 수정 코드로 인해 발생하는 문제입니다. 자체 수정 코드는 명령 검색 과정을 복잡하게 만들 수 있으며, CPU의 명령 캐시 일관성을 깨뜨릴 수 있습니다. 이를 해결하기 위해서는 CPU의 명령 캐시를 무효화하거나 자체 수정 코드 사용을 최소화해야 합니다. 이러한 접근 방식은 명령 검색 효율성과 일관성을 유지하고 x86 시스템의 성능을 향상시키는 데 도움이 될 것입니다.

반응형
Comments