head.S에 시작은
.rept 8
mov r0,r0
.endr
로 시작을 하는데요. NOP을 8번 수행하라는 뜻입니다.
딜레이를 가지기 위한 코드라고 생각을 했는데요.
정확히 뭘위해 이렇게 시작하는지 갑자기 호기심이 드네요 :)
딜레이를 위한다라면..정확히 계산되서 8번 실행하는건지도 궁금하구요 :)
댓글 27
-
백창우
2010.05.03 14:33
-
백창우
2011.08.15 03:55
이글이 좀 공격성을 뛰는 글이라 부연 설명이 있어야 하겠네요. ^^;;
겉멋이 잔뜩 들어간 대표적인 코드가 Xen의 소스 코드입니다.
아직 미숙할때 보시면 해당 코드들이 멋져 보이는데, 수준이 어느정도 올라가셨을때 보시면 손발이 오그라든답니다.
-
백창우
2011.08.15 03:31
헐... 전의 글이 떠버렸네요.
정확하게 말한다면 pipeline을 비워주기 위한 행위입니다.
운영체제 커널을 개발할때는 어떠한 가정을 해서는 안됩니다.
예를들면 "bootloader가 이건 해줬을 테니깐 이건 안해줘도 되겠지?" 같은 가정을 해서는 안됩니다.
그렇기 때문에 모든것을 처음부터 시작하기 위해 pipeline을 비워주는 것입니다.
-
백창우
2011.08.15 10:44
ㅎㅎ 제 댓글이 한발 늦었네요.
-
백창우
2011.08.15 10:43
아닙니다. 컴파일러에 따라 ABI가 다를수 있고, ABI에 따라 calling convention이 다를 수 있습니다.
calling convention을 설명하자면 함수를 호출하는 규약을 말합니다.
ARM 같은 경우 r0-r3까지는 caller saved register로 사용되어 함수 인자를 넘기던가 혹은 함수내 variable등을 할당하는데 사용됩니다. 그리고 만약 해당 register 값들을 유지하고 싶다면 caller 함수 내에서 해당 register를 stack에 save하고 호출해야됩니다. 왜냐하면 callee에서 해당 register들을 아무런 제약없이 쓰버리기 때문입니다. 그래서 caller saved register로 부른답니다.
r4-r11은 callee saved register에 해당합니다. 함수내 각종 변수들을 위해서 사용되고 callee 함수가 callee saved register를 callee 함수내에 사용하게 된다면 해당 register 값을 stack에 push하고 함수가 끝나는 시점에 stack에 save된 값들을 복원해주어야 합니다.
그래서 callee saved register는 caller가 callee 함수를 호출하고 caller에게 다시 돌아왔을때 해당 register들의 값이 callee 함수를 호출하기 전과 동일하다는 것을 보장받게 됩니다.
r12(ip)는 callee saved register로 사용될수도 있고 intra-procedure-call scratch register라고 해서 callee 내에서 다른 subroutine을 call할때 주소 계산을 위한 temporary register로 사용될수도 있습니다. r13, r14, r15는 잘 아실테고요.
이렇게 함수를 호출할때 각종 register의 사용 용도, stack의 사용 방식, prologue 과정, epilogue 과정등 모든 함수 호출에 대한 규약을 정의해놓은 것이 바로 calling convention입니다. 이러한 calling convention은 컴파일러가 지원하는 ABI에 의해 다를 수 있는데, ABI는 Application binary interface의 약자로써 calling convention을 포함한 compiler의 architecture 지원 API의 종류 및 naming과 argument등 거의 모든 내용을 규정하고 있는 규약입니다.
이러한 규약들은 compiler에 의해 적용되게 되고, arm 같은 경우는 r0-r14까지 그냥 일반적인 범용 register이기 때문에, architecture에서 어떤 register를 어떤 용도로 사용하라는 특별한 규약이 없습니다. 때문에 compiler에 따라 ABI가 달라질 수 있고, 이로인해 calling convention도 달라질 수 있고, 이로인해 과거의 arm architecture가 calling convention에 영향을 받는 architecture였다면 mov r0, r0가 반드시 필요할 수도 있다는 것입니다.
-
유경환
2011.08.15 10:03
>>저 calling convention이라는 용어를 잘 이해를 못해서 그러는데..
>>음.. mov r0, r0를 8번 루프도는 코드로 만들경우 ARM 컴파일러에 따라 최적화 한답시고 하다가>>똑같은일 8번 하는 코드를 한번으로 바꿔버리는 불상사가 컴파일 될때 발생하는 경우가 존재해서
>>pipeline을 초기화하기 위한 mov r0, r0를 명시적으로 코드에 8번 집어넣었다는 뜻인가요.
라는 수준낮은 질문을 했었습니다. ^^;;
-
백창우
2011.08.15 10:44
거의 그렇다고 봅니다.
-
홍문화
2011.08.15 21:57
갑자기 이런 생각이 떠올랐습니다. 러셀 킹은 독실한 크리스찬이 아닐까?
"새술은 새부대에"라는 성경 구절이 떠올라서요.
새코드(커널)는 새부대(파이프라인을 다 비우고)에!!!
-
홍문화
2011.08.15 21:13
유경환님께서 걸어주신 블로그의 내용을 보고 오히려 의문이 더 커져버렸습니다. 켘
"부트로더에서 영향받은 파이프 라인" 이라는 표현을 사용하셨는데 정확히 어떤 내용을
말씀 하시는건가요? cdecl 함수 호출 규약으로 설명한 블로그 내용을 부트로더에서 커널로
넘어오는 과정에 반영 해보면 6번 까지가 부트로더의 코드가 될 것입니다. 7번 부터는
커널 코드가 될 것이고 6번과 7번 사이(로더와 커널 사이)에서 파이프라인에 문제가 될 소지는
없어 보입니다. 로더에서 커널로 넘어오는 과정에서 함수 호출 규약에 있어 문제가 될만한
소지는 로더에서 커널 매개변수로 넘겨 준 r1, r2 레지스터의 값이 될 것입니다. 커널에서 head.S는
어셈코드로 r1, r2에 어떤 값이 넘어 올지를 이미 fix하고 있습니다.
(r1 => save architecture ID, r2 => save atags pointer) 하지만 로더에서 커널로 점프하는 코드는
C로 작성이 되어있고 컴파일러의 함수 호출 규약 문제로 매개변수의 값을 r1, r2에 바꿔서
저장 한다면 커널은 잘못된 매개변수 값을 사용하게 될것입니다. 이러한 함수 호출 규약 문제를
nop 8개로 해결 할 수는 없을테고...
이번 경험을 통해 새삼 느끼는 것이지만 내가 만든 코드도 시간이 지나면 왜 이렇게 했지? 라고
잊어버리는데 다른 사람의 생각을 알아내는게 여간 힘든게 아닌것 같습니다. ㅋ
-
유경환
2011.08.15 10:18
여기서 calling convention이라는 용어를 써서 주석을 달아놓았지만 결국 처음에 백창우님이 답글달아 준대로,
이 부분을 호출한 bootloader에 영향받은 pipleline상의 모든 instruction을 결국 초기화한다는것을
"sort out different calling conventions" 이라는 용어를 써서 주석을 달아놓은것이 아닐까요?
좀더 정확히는 "sort out different calling conventions of pipeline". "부트로더(different calling conventions)에서 영향받은 파이프 라인을 싸그리 정리해버려!!(sort out)" 이렇게 ..^^;;
-
백창우
2011.08.15 09:39
ㅎㅎㅎ 정정하겠습니다.
아래와 같이 2.2.0대 커널부터 해당 코드가 들어 있네요. ^^
주석을 보면 당시 bootloarder 또는 kernel을 컴파일한 컴파일러들의 calling convention이 다른 부분을 해결하기 위해서 인것 같습니다.
의미 없는 코드가 들어간것을 보니 당시 arm architecture는 calling convention에 의해 영향을 받았나 봅니다.
======================================
/*
* sort out different calling conventions
*/.align
.globl _start
_start:
start: mov r0, r0
mov r0, r0
mov r0, r0
mov r0, r0
mov r0, r0
mov r0, r0
mov r0, r0
mov r0, r0
b 1f
.word 0x016f2818 @ Magic numbers to help the loader
.word _start
1: teq r0, #0
beq 2f
mov r4, #0x02000000
add r4, r4, #0x7C000
mov r3, #0x4000
sub r3, r3, #4
1: ldmia r0!, {r5 - r12}
stmia r4!, {r5 - r12}
subs r3, r3, #32
bpl 1b
2: adr r2, LC0
ldmia r2, {r2, r3, r4, r5, r6, sp}
add r2, r2, #3
add r3, r3, #3
add sp, sp, #3
bic r2, r2, #3
bic r3, r3, #3
bic sp, sp, #3
adr r7, start
sub r6, r7, r6 -
홍문화
2011.08.15 09:29
아~ 그렇군요.
이제 좀 궁금증이 풀렸습니다. ^^;
-
백창우
2011.08.15 09:03
아래 댓글로 정정합니다.
-
유경환
2011.08.15 08:40
저도 분석하다가 궁금하여 백창우 님이 예전에 분석한 head.s쪽 주석을 참고했는데, 이부분 주석이 "I-cache 를 초기화 하기위해서" 라고 적혀있었습니다. pileline 을 비우는것과 I-cache를 초기화 하는것과 관련하여 설명좀 해줄수 있을까요? ^^;;
그리고, 파이프라인과 관련하여서도 아키텍쳐에 따라 3단, 5단, 6단으로 파이프라인의 스텝이 다른데 mov r0, r0(NOP)을 8회 하는 이유는 해당 라인이 아키텍쳐별로 범용성을 띄기위해 8회라는 reasonable한 loop를 도는것인지요? 8회라는 숫자도 궁금하고..
대략 짐작은 가는데 찜찜한 느낌이 남는 코드같습니다. 설명 부탁드립니다.
-
유경환
2011.08.19 17:59
8 stages in normal pipeline1. Fe1 – Address is sent and instruction received2. Fe2 – Much of the branch prediction goes here3. De – Decode instruction4. Iss – Read registers and issue instruction5. Sh – Perform shift operations6. ALU – Perform integer operations7. Sat – Saturate results8. WB – Write back data to registers위는 ARM11의 Pipeline 8 stage입니다. 레지스터에 저장되는 스텝은 8번째 마지막 순간이군요.반면 read register는 4번째 스텝이고, 그래서 생각해보면 같은 컴파일러에서 컴파일한 바이너리는알아서 잘 정리되어 write한 레지스터값을 읽어오도록 최적화 되겠지만,이종의 컴파일러에서 혹은 같은 컴파일러 라도 각각 컴파일한 실행이미지간에 call을 한다면calling convention이 무너질수 있겠네요예를 들어(그저 예일뿐)<loader>-------------------------mov r0, #0xffmov pc, kernel_entry-------------------------<kernel>-------------------------kernel_entry :add r1, r1, r0-------------------------이러면 덧샘연산을 위해 r0를 read 당시에는 0xff가 아닌 값일수 있기때문에 calling convention이 무너진것이고이를 보호하기위해서 아무 의미없는 mov r0, r0를 8번 해준것(write back to register 를 할수 있는 clock확보)아닌가 생각합니다. -
홍문화
2011.08.20 02:24
와~ 대박!!!
유경환님의 끈기에 무한 존경을 표하는 바입니다. ^^;
-
송원준
2011.08.20 03:29
와 대단하십니다!
mov r0,r0 x 8번 명령어의 이유가
스터디 하시는 다른 분들께도 흥미로우셨던것 같습니다. 다들 샅샅이 공부하시는 것 같아 제가 부끄러워지는군요 ^^
러셀 킹 형님께서 종결 지을 수 있도록 힘써주신 유경환님의 끈기에 박수를!
-
백창우
2011.08.20 09:01
스터디가 낼수 있는 시너지의 정말 좋은 모범 사례인것 같습니다. ^^
한수 배웠습니다.
-
리누즈박
2013.07.10 21:41
아하~N이 그런 의미였군요~!!
일하면서 아무생각없이 0xC0008000에 zImage를 다운로드 했었는데.. 그게 다 그런 이유 때문이었군요.
아직 내용이 전부 그려지지는 않지만 몰랐던 사실을 알게되어 정말 기분 좋습니다.
정말 감사합니다.
-
제이
2013.07.22 10:19
왜 이 글이 상위에 올라왔나 했더니 ㅎㅎ
잡담같지 않은 깔끔한 정리네요.
감사합니다.
-
맥주
2013.07.10 18:01
엄청 오래전에 글이네요. 해피앤딩의 러브스토리였습니다. NNNN 은 임의의 주소를 표현한 것 같네요. 커널은 물리메모리의 0x8000 에 위치합니다. 페이지 테이블은 0x4000 에 만들구요. 32bit 구조에서 phy address 또한 4G 영역을 가지는데, 실제 메모리도 이 영역내에 있겠지요. CPU에 메모리 컨트롤러가 있고, HW 설계하는 분이 각 뱅크에 어떤 디바이스를 붙이는가에 따라 실제 물리메모리의 시작 주소가 달라 집니다. vir address 는 0xC000000 이 물리메모리의 시작 주소와 맵핑됩니다. 그래서 커널의 시작 주소를 말 할때, 0xC0008000 이라고도 하죠.
여담으로 0xC0000000 의 vir address 는 4G의 영역중에 3G의 시작 부분인데, 그래서 리눅스에서 실제 물리메모리와 바로 맵핑 할 수 있는 vir address 가 1G 밖에 안됩니다. ( 3G~4G까지, 실제로 뒤 부분은 여러가지 용도로 사용하기 때문에 1G보다 작죠)
다 아는 내용의 잡답입니다.
-
리누즈박
2013.07.10 12:46
질문이 있습니다.
위 내용 중 0xNNNN8020, 0xNNNN8000 이 있는데
NNNN은 무슨 뜻인가요?
그리고 왜 8000부터 시작하나요?
-
유경환
2011.08.20 00:11
러셀 킹형 답이 있었네요..;;
Author: Russell King - ARM Linux
Date: 2006-08-02 23:10 +900
To: liang ming-chuan
CC: linux-arm-kernel
Subject: Re: the purpose of repeat mov r0, r0옛날옛날에 어떤 거지같은 부트로더가 있었는데 그놈은 이미지 올려놓은 램주소에서
8 Word(32byte = 0x20)만큼의 하위 주소를 call해 버렸었고 올려놓은 주소에서
offset(0x24~0x30)사이에는 magic number를 찾았다는군요..
부글부글..
그래서 그런 거지같은 부트로더들 맞춰줄려고 head.s에 의미없는 instruction(mov r0, r0)를
8번(8 Word)해서 소비해주시고, 뜬금없고 의미없는 8번의 삽질후
.word 0x016f2818 @ Magic numbers to help the loader
.word start @ absolute load/run zImage address
.word _edata @ zImage end address
매직넘버와 address를 offset(0x24~0x30 = 12Byte = 3Word)에 집어넣는 구조가 되어버렸다는 것같군요.
부글부글..
-
유경환
2011.08.19 19:41
음 그럼 추측이 또 잘못됐군요.. ^^;
-
myskan
2011.08.19 18:52
말씀하신 예는(dependency) ARM 프로세서의 경우 하드웨어 레벨에서 stall 과 forwarding 으로 해결할 것입니다.
-
신C
2013.07.12 15:15
아~ 그렇군요.. 제 생각속에 여러가지 흐릿한 그림들이 명확해 지네요. ^^ 감사합니다.!
-
이한울
2014.05.31 15:31
잘 봤습니다.
.
delay일수도 있지만 첫 시작에서 이런 경우는 delay가 보통 아닙니다.
아키텍쳐에 따라 다른데 H/W적인 제한 때문에 저럴 수도 있고,
pipeline을 비워주기 위해서 저렇게 하는 경우도 있습니다.
이도저도 아닌 경우에는 개발자의 겉멋 때문에 들어갈 수도 있습니다.