우선 이포스팅을 이해하기 위해서는 전에 포스팅했던 글을 읽어보아야 할 것이다.
http://rechoco.egloos.com/4522442 MMU가 날 놀린다
짧게 요약하자면, arm11 mmu와는 달리 cortex series 에서는 mmu를 on 하자마자 prefetch를 하도록 구성되어 있다.
하여 boot로더와 같이 physical로 동작하던 코드에서 mmu를 enable할 경우 data abort가 발생하게 된다.
*메모리맵
DRAM의 physical : 0x40000000
Virtual mapping : 0x40000000 = 0x80000000(Cached) = 0xA0000000(Non-cached)
*문제코드
mcr p15, 0, r2, c1, c0, 0 ; Restore System Control Register (MMU Control)
mov pc, r1 ; Jump to kernel address(r1 = virtual)
*동작 상황
- arm11
PC(0x40000004) - MMU ON
0x40000008(PC+4)발생 -> mmu로 전달 -> 0x80000008(virtual로 변환)
0x80000008의 코드 -> mov pc,r1(phy:0x40000008 에 해당하는 코드)
r1으로 점프
- cortex
PC(0x40000004) - MMU ON
**After a CP15 c1 instruction enables the MMU, the processor flushes all following instructions in the pipeline. The processor then begins refetching instructions,
fetch translated (PC+4) 발생 -> mmu로 전달 -> 0x80000008(virtual로 변환)
fetch translated (PC+8) 발생 -> mmu로 전달 -> 0x8000000C(virtual로 변환)
fetch translated (PC+C) 발생 -> mmu로 전달 -> 0x80000010(virtual로 변환)
fetch translated (PC+10) 발생 -> mmu로 전달 -> 0x80000014(virtual로 변환)
fetch translated (PC+14) 발생 -> mmu로 전달 -> 0x80000018(virtual로 변환)
Data abort.........
차이점이라면..processor안에 파이프라인을 새롭게 채우느냐 안채우느냐의 차이인데....
arm11이라면 mmu를 켠시점에서 채워지는 코드들은 점프 한 후의 코드일 것이고..
cortex라면 채워지는 코드가 mmu를 켠 시점의 아랫부분의 코드들이므로 valid한 코드는 아닐것이다.
여기서 재미있는 코드를 하나 보자.
(해당 코드는 망고보드 BSP로 SMDKV210에 있는 코드이다.)
mmu를 켜고 SleepState_WakeAddr 으로 점프를 뛰기 위한 코드이다.
; Set MMU Table
; --------------------------------------------------------------------------------------------------------------
; There are some CPUs having pipeline issues that requires identity mapping before turning on MMU - (ex. Cortex-A8)
; We'll create an identity mapping for the address which we'll jump to when turning on MMU on.
; And then we'll remove the mapping after we jump to the virtual address.
; ------------------------------------------------------------------------------------------------------------------
이 코드를 작성한 분이야 말로 MMU컨트롤을 "제대로" 이해하고 계신 분이 아닐까한다.
cortex mmu를 켤때 뭔가 문제가 있다 라는 것을 파악하고 아래 코드를 작성하셨다.
line by line으로 분석해 보자.
ldr r3, [r10, #SleepState_MMUTTB0] ; TTB Register0
; r3에 TTB0 레지스터의 값을 넣는다. (backup/restore 루틴중 일부이므로 코드자체에 대한 의심은 버려도 된다.)
ldr r12, =0xFFF00000 ; (r12) = mask for section bits
; Level 1 Page Table을 구성하는 테이블 아키텍쳐중 상위 12개 비트를 base address로 쓰는 테이블은 Section Entry이다.
; 0x100000 = 1MB 니까 1MB 단위로 표현하려면 상위 12비트면 ok.
; 왜 섹션 엔트리를 사용했는지는 나중에...
and r9, pc, r12 ; physical address of where we are
; NOTE: we assume that the startup function never spam across 1M boundary.
; 현재 PC값이 있는 코드 영역의 메모리를 1M section엔트리로 만들기 위해 상위 12bit를 and 했다.
orr r0, r9, #0x2
; Section Entry의 shuld be one (SBO) field를 채움
orr r0, r0, #0x400 ; (r0) = PTE for 1M for current physical address, C=B=0, kernel r/w
; 주석상에는 cache와 buffer 옵션을 모두 끄고 kernel r/w속성을 주었다고 서술한다.
; 0x400 이므로 C,B = 0 이며, 접근권한(AP)가 01이므로 Supervisor 모드에서 R/W 속성으로 접근된다.
add r7, r3, r9, LSR #18 ; (r7) = 1st level PT entry for the identity map
; 현재 r9 에는 PC가 위치한 메모리영역의 base address가 들어있고...
; r3 = TTB0
; base address를 LSR #18 한다. -> base addr >> 18 하면 딱 base address만 남는다.
; 즉 r7에는 base address에 해당하는 엔트리 위치가 연산되게 된다.
; * 1M 단위 엔트리의 갯수는 총 4096로 4G의 virtual address를 표현하게 되며
; * 4096 * 4 바이트가 되니까 Page Table Entry의 총 사이즈는 16K바이트이다.
; * TTB0 어드레스 부터 16K 만큼은 page table entry들이 들어있고
; * 내가 쓸 엔트리의 위치는 TTB0로 부터 1M단위로 쪼개지는 baseaddress 위치에 있겠다.
ldr r10, [r7] ; (r10) = saved original content of the 1st-level PT
; 내가 쓸 엔트리 위치의 원래값을 임시적으로 r10으로 빼놓고 (나중에 다시 복원해줄꺼다)
str r0, [r7] ; temporary save identity map to 1st-level PT
; 새롭게 내가 만든 엔트리를 집어넣는다...
mcr p15, 0, r6, c3, c0, 0 ; Restore Domain Access Control Register
mcr p15, 0, r5, c2, c0, 2 ; Restore TTB Control Register
mcr p15, 0, r4, c2, c0, 1 ; Restore TTB Register1
mcr p15, 0, r3, c2, c0, 0 ; Restore TTB Register0
mov r0, #0x0
mcr p15, 0, r0, c8, c7, 0 ; Invalidate I & D TLB
; 상위 6줄은 복구 코드 이므로 신경안써도 되는 부분....
ldr r0, =VirtualStart
; r0에 점프할 가상주소를 넣는다. 이 주소에는 r10의 값을 다시 복원해주는 코드가 있다.
cmp r0, #0 ; make sure no stall on "mov pc,r0" below
mcr p15, 0, r2, c1, c0, 0 ; Restore System Control Register (MMU Control)
; 자 드디어 mmu를 켰다.
; 이 순간부터 아래 다섯줄이 fetch가 일어 날 것이다.
mov pc, r0 ; & jump to new virtual address
nop
nop
nop
nop
nop
; fetch된 명령어 5줄중 첫번째 루틴은 virtaulstart label로 점프다.
; Return to WakeUp_Address
VirtualStart
; temporarily set the stack pointer to use OAL functions
;ldr r3, =PWRCTL_COREDATA_PA_STADDR ; Sleep Data Area Base Address
; 이 코드는 버그 인듯... 이미 mmu가 켜진상태에서 physical로 접근하여 뭔가를 시도한다는건 조금 낯설다
; 실제 돌려봐도 아래 ldr 명령어에서 시스템이 뻗는다.
; 그래서 강제로 virtual로 변환해 주었다
ldr r3, =0xA01F8100
ldr sp, [r3, #SleepState_SVC_SP] ; Restore Stack Pointer to use OALPAtoVA
ldr r4, [r3, #SleepState_WakeAddr] ; (r4) = Return Address (VA)
; 원래 뛰고자 했던 함수의 위치 와 OALPAtoVA 함수를 사용하기위해 스택을 복원해 준다
; VA of the translation table for identity map
mov r0, r7 ; (r0) = Physical Address
mov r1, #0 ; uncached
bl OALPAtoVA ; Translate PA to VA
; r7 에 현재 PTE의 physical 주소가 들어있으니 복원해주기위해 Virtual로 변환
; restore the identity map
str r10, [r0] ; (r10) = saved content of the 1st-level PT
; PTE를 원래대로 돌려놓았다.
; Invalidate I & D TLB
mov r0, #0x0
mcr p15, 0, r0, c8, c7, 0
; go back
mov pc, r4 ; Jump to Virtual Return Address
; 이제 진정으로 뛰기 원했던 커널코드(r4 = SleepState_WakeAddr )로 점프!!
코드 해석은 끝났는데.. 원초적인 질문은 풀리지 않았다.
그리고 새로운 질문이 하나 생겼다.
1. cortex환경에서 mmu on을 하고 virtaul address로 점프하는 코드를 실행했을때 시스템이 죽는 현상은 왜인가.
2. 또한 망고 코드에서 PTE를 임시로 만들어서 대체 했을 경우에는 mmu on시 왜 문제가 안생기는가.
망고 코드에서 역분석을 시작 해본다.
망고코드에서는 mmu를 on하기 직전 현재 코드가 돌고 있는 메모리 영역에 해당하는 entry값을
pc주변의 위치로 돌려 놓는다.
이렇게 되면 mmu가 on이 되어 virtual로 변환이 된 주소도 현재 코드 위치로 넘어오게 된다.
결과적으로 mmu on에 의해 새롭게 fetch되는 명령어들이 정상적으로 physical 영역에 존재하게 되는것이다.
그렇다면 기존 entry에 해당하는 코드가 현재 physical한 메모리 상에 유효하지 않다는 말로 해석할 수 있다.
mmu를 on 했을때 TLB에서 hit가 나지 않는다면 PTE를 검색하여 virtual로 변환을 할 것이고
따라서 mmu entry를 수정해 주지 않는 방식을 사용했을 경우
변환된 주소에 위치하는 코드들이 내가 원하는 코드가
아닐 수도 있는 것이다.
그렇게 되면 abort가 발생하게 되겠지...
***
기존 PTE의 값은 0x5EF49801 이다.
해석해보면..일딴 맨뒤가 1인걸로 봐서는 coarse 엔트리이고
코어스인경우 L2의 base를 가리키고 있고 31~10까지가 base이므로
0x5ef49800 에 L2 table이 존재할 것으로 예상된다.
그러나 해당주소의 데이터가 현재 0으로
pagefault 엔트리이며 접근할 경우 prefetch 또는 data abort를 생성하도록 되어있다.
결국 지금 내 물리코드가 도는 지점의 위치가 virtual과 매핑이 되지 않은 부분이라고 생각해야 한다.
그럼 내 물리코드는 왜 매핑이 되어있지 않을까?
기존 PTE를 사용하면 prefetch abort 가 발생한다.
prefetch abort에 대해서 히언님의 사이트에서 힌트를 얻을 수 있다.
http://recipes.egloos.com/5035801
3. Prefetch Abort Handler
그러면 Prefetch Abort Handler같은 거에서는 어떤 일을 하면 좋을까요? 또 별거 있나요? Prefetch Abort Handler나 Data Abort Handler에 들어오면 보통 문제가 발생했다는 것을 외부에 알리는 일을 하거나, information을 남겨놓는 일을 남겨 놓지요. 안 만나면 좋은 Exception이니까 어떻게든 만천하에 알려서 문제가 수정될 수 있도록 하는 routine을 끼워 넣는 일이 보통입니다. Debugging 용도의 Code들이 삽입되는 게 보통인데요,MMU를 쓰는 경우에는 Prefetch Abort의 경우 스와핑으로 잘 알려진 Demand Loading을 사용하느라 그런 일이 벌어질 때도 있어요. Demand Loading이란 건 XIP를 하는 Memory가 너무 작으니까, Software를 모두 Loading하는 게 아니라, 실제 실행 해야 하는 부분만 XIP Memory에 Loading 해서 사용하는 고급(?) 기법이지요. PAbort의 경우에도 진입했다가 복귀할 때는 Undef Exception과 같은 처리를 하면 되니까,
결국. 내가 MMU를 켜는 시점 메모리 영역에서 발생하는 virtual address가
실제 물리 메모리상에 코드가 올라와 있지않은 영역을 참조하도록 기존 PTE가 설정되어 있는 것이다.
따라서 기존 PTE를 따라 갔다면 매핑이 되어 있지 않은 주소이므로 prefetch abort가 발생하게 된다!!
**
마지만 아주 조그만 궁금점은. 맨 처음 부팅시에도 매핑이 되어 있지 않을텐데 abort가 발생하지 않는것이다.
커널의 핸들러가 이런경우엔 물리메모리에 해당코드를 올리려나?
***
WinCE가 mmu를 설정하는 부분 을 포스팅 하다가 알아냈다.
WinCE6.0 , 7.0 커널 스타트업 코드에 새롭게 임시 맵핑하여 mmu를 켜는 코드가 모두 들어있다.
결국 그냥 켜는 것이 아니라 저렇게 하는게 원래 맞고,
기존에 우리가 사용하던 코드가 운이좋아 실행된것 같다.
****
WinCE 가 on-demand paging을 한다고 하는데...
이부분에서 코드를 어떻게 올리는지도 같이 분석하면 좋을것 같다.
올리면서 mmu도 갱신되지 않을까?
demand paging(요구 페이징) 을 읽어보니, page fault가 발생했을때 커널이 처리해 주어야 하는데,
커널이 깨어나는 도중이기때문에 처리를 해줄수가 없게 되고.
그러므로 flow가 멈추는 것으로 결론 지어도 무리가 없을듯 하다.




덧글