Why should I know this?

Memory Barrier for self-modifying code 본문

Knowledge/Architecture

Memory Barrier for self-modifying code

die4taoam 2020. 1. 19. 18:23

ARM 에서는 Self-Modifying code를 사용할 경우를 위한 Guide를 제시하고 있다.

 

ARM Cortex-M Programming Guide to Memory Barrier Instructions

위 링크에서 다루는 내용은 ARM cortext M0 이후 세대 M3 ARMv7 이후 제품군에서 필요할 수도 있는 Memory Barrier 명령군에 대해서 다루고 있다. 상업적으로 말하면, ARMv7 이후 제품들이 실질적으로 안드로이드 스마트폰에서 사용되었으므로, 거의 대부분의 Self-Modifying code를 Android에서 구현하고자 하는 개발자들이 알아두면 좋을 내용이다.

 

 

ARMv7 이후 제품군에서는 Super-Scalar (슈퍼스칼라 or 슈퍼스케일러) 아키텍처를 채택하여 Memory-Barrier 계통의 명령어들이 추가되었다. 왜냐면, CPU가 명령을 순차실행 in-order 하도록 설계되었으나 이를 Fetch-Decode-Execute의 Pipeline을 추가하여 성능 향상을 꾀하면서 각 Pipeline 간 영향을 미치는 Instruction - 예를 들면 동일한 주소에 대한 Store와 Load - 이 성능의 병목지점이 되었기 때문이다.

 

때문에 Super-Scalar 아키텍처는 기존과는 다르게 깊은 Depth의 Pre-Fetch를 하고 각 Instruction이 상호 영향받지 않는 처리 순서로 처리한다. 이를 비순차실행 out-of-order 라고 부르는 것이다.

 

여기서 Self-Modifying Code를 작성하는데 아키텍처 레벨의 문제가 발생할 여지가 생기게 된다.

다음 그림을 보자.

 

출처 - ARM Information Center

 

Self-Modifying Code는 Runtime에 특정 메모리에 실행할 명령어들을 기록하고, 이후 해당 메모리를 실행하는 코드를 지칭한다. 이와 같은 방식이 Super-Scalar를 채택한 CPU 아키텍처에서는 메모리에 코드를 기록하는 과정에서 완성되지 않은 메모리 주소에 대한 Pre-fetch가 이루어질 가능성이 존재하는 것이다.

 

// C - Memory Barrier for ARM
// 출처 - linux kernel include/asm-arm/system.h
#if __LINUX_ARM_ARCH__ >= 7
#define isb() __asm__ __volatile__ ("isb" : : : "memory")
#define dsb() __asm__ __volatile__ ("dsb" : : : "memory")
#define dmb() __asm__ __volatile__ ("dmb" : : : "memory")
#elif defined(CONFIG_CPU_XSC3) || __LINUX_ARM_ARCH__ == 6
#define isb() __asm__ __volatile__ ("mcr p15, 0, %0, c7, c5, 4" \
                                    : : "r" (0) : "memory")
#define dsb() __asm__ __volatile__ ("mcr p15, 0, %0, c7, c10, 4" \
                                    : : "r" (0) : "memory")
#define dmb() __asm__ __volatile__ ("mcr p15, 0, %0, c7, c10, 5" \
                                    : : "r" (0) : "memory")
#else
#define isb() __asm__ __volatile__ ("" : : : "memory")
#define dsb() __asm__ __volatile__ ("mcr p15, 0, %0, c7, c10, 4" \
                                    : : "r" (0) : "memory")
#define dmb() __asm__ __volatile__ ("" : : : "memory")
#endif

 

기타 - v8 에서 발췌한 ARM용 Memory Barrier들.

void Assembler::dmb(BarrierOption option) {
  if (CpuFeatures::IsSupported(ARMv7)) {
    // Details available in ARM DDI 0406C.b, A8-378.
    emit(kSpecialCondition | 0x57FF * B12 | 5 * B4 | option);
  } else {
    // Details available in ARM DDI 0406C.b, B3-1750.
    // CP15DMB: CRn=c7, opc1=0, CRm=c10, opc2=5, Rt is ignored.
    mcr(p15, 0, r0, cr7, cr10, 5);
  }
}

void Assembler::dsb(BarrierOption option) {
  if (CpuFeatures::IsSupported(ARMv7)) {
    // Details available in ARM DDI 0406C.b, A8-380.
    emit(kSpecialCondition | 0x57FF * B12 | 4 * B4 | option);
  } else {
    // Details available in ARM DDI 0406C.b, B3-1750.
    // CP15DSB: CRn=c7, opc1=0, CRm=c10, opc2=4, Rt is ignored.
    mcr(p15, 0, r0, cr7, cr10, 4);
  }
}


void Assembler::isb(BarrierOption option) {
  if (CpuFeatures::IsSupported(ARMv7)) {
    // Details available in ARM DDI 0406C.b, A8-389.
    emit(kSpecialCondition | 0x57FF * B12 | 6 * B4 | option);
  } else {
    // Details available in ARM DDI 0406C.b, B3-1750.
    // CP15ISB: CRn=c7, opc1=0, CRm=c5, opc2=4, Rt is ignored.
    mcr(p15, 0, r0, cr7, cr5, 4);
  }
}

 

 

Cacheflush 와의 차이점.

 

// Linux Kernel 3.10.x 
static inline int
do_cache_op(unsigned long start, unsigned long end, int flags)
{
	struct mm_struct *mm = current->active_mm;
	struct vm_area_struct *vma;
	if (end < start || flags)
		return -EINVAL;
	down_read(&mm->mmap_sem);
	vma = find_vma(mm, start);
	if (vma && vma->vm_start < end) {
		if (start < vma->vm_start)
			start = vma->vm_start;
		if (end > vma->vm_end)
			end = vma->vm_end;
		up_read(&mm->mmap_sem);
		return flush_cache_user_range(start, end);
	}
	up_read(&mm->mmap_sem);
	return -EINVAL;
}

// Linux Kernel 3.13.x

static inline int
__do_cache_op(unsigned long start, unsigned long end)
{
	int ret;
	do {
		unsigned long chunk = min(PAGE_SIZE, end - start);
		if (signal_pending(current)) {
			struct thread_info *ti = current_thread_info();
			ti->restart_block = (struct restart_block) {
				.fn	= do_cache_op_restart,
			};
			ti->arm_restart_block = (struct arm_restart_block) {
				{
					.cache = {
						.start	= start,
						.end	= end,
					},
				},
			};
			return -ERESTART_RESTARTBLOCK;
		}
		ret = flush_cache_user_range(start, start + chunk);
		if (ret)
			return ret;
		cond_resched();
		start += chunk;
	} while (start < end);
	return 0;
}



// Linux Kernel Version 3.16

static inline int
__do_cache_op(unsigned long start, unsigned long end)
{
	int ret;
	do {
		unsigned long chunk = min(PAGE_SIZE, end - start);
		if (fatal_signal_pending(current))
			return 0;
		ret = flush_cache_user_range(start, start + chunk);
		if (ret)
			return ret;
		cond_resched();
		start += chunk;
	} while (start < end);
	return 0;
}

cacheflush는 실패할 수 있다. kernel 3.13 버전에서는 cacheflush의 마지막 진행 상황까지를 threadinfo에 보관해 호출자에게 재시도 할 수 있는 정보를 전달해줬으나 문제는 이런 Kernel-User 사이의 인터페이스를 사용하는 개발자가 없다는 것이다. 때문에 이후 커널 버전에서는 cacheflush가 실패하면 그냥 실패처리를 하고 반환을 해버린다. 개발자는 이에 대한 처리 내역을 알 길이 없고 다시 cacheflush를 호출해야 한다. 문제는 cacheflush가 꽤나 무겁다는것... 개발자는 cacheflush를 사용할 때, 실패할 경우에 대한 처리를 반드시 해줘야한다. cacheflush를 다시 시도할 지, 아니면 다른 어떤 방법을 취할지, 그 선택과 결과는 모두 개발자의 몫이다.

 

 

앞서 Memory-Barrier를 다뤘는데, Memory-Barrier는 메모리에 대한 접근흐름을 제어하는 명령군이다. 다시 말해 Cacheflush를 사용할 때 의도하는 것처럼 Cache와 Memory 간의 sync를 맞추거나 Cache를 비우는 동작은 하지 않기 때문에, 만약 cache-memory sync를 맞추거나 cache를 비우고자 한다면 cacheflush를 호출해야 해야한다.

 

 

INTEL과 ARM의 차이점

ARM의 경우 Memory-Barrier 명령군은 순차 실행을 지원하는 역할을 한다. 명령군 내부의 store 와 load 명령은 cache에 의존적일 수 있다. 이와 반대로 INTEL에서 Memory-Barrier는 순차 실행 지원 뿐 아니라 명령으로 구축된 메모리 배리어 내부의 load 와 store 명령을 메모리와 직렬화 하는 기능도 함께 내포하고 있다.

 

INTEL은 Memory-Barrier 명령군과 별도로 Lock #Prefix 혹은 xchg 계통의 명령으로 atomic operation을 지원한다.

ARM도 마찬가지로 특수 명령을 통해 atomic operation을 지원한다.

 

INTEL의 Memory-Barrier는 Block단위로 처리 할 수 있다는 장점이 있다는것?

이유는 딱히 모르겠다.

 

 

+@로 구형 아키텍쳐 ARMv7 에서 Cacheflush를 사용할 때 생기는 문제와 해결책은 여기서 참고하세요.

https://die4taoam.tistory.com/56

 

ARMv7 에서 Cacheflush를 사용할 때 주의점

ARMv7 아키텍처에서 동적생성된 코드를 실행하거나 복호화 후 실행 시에 알 수 없는 이유로 SIGSEGV 나 SIGILL이 발생하는 경우가 있습니다. 빈도 또한 일정치 않고, 디버깅조차 어려우므로 이곳에 해

die4taoam.tistory.com

 

'Knowledge > Architecture' 카테고리의 다른 글

Linux Thread Local Storage 공부 및 적용  (0) 2022.12.19
Comments