Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

74261
1501
219116


[ARM64] GCC: 특정 함수를 최적화하지 않기 - optimize("O0") 옵션 ARM 프로세서를 위한 C 최적화

코드를 작성한 후 실행을 하다보면 가끔 자신이 작성한 함수의 심벌이 사라지거나, 가끔 예상을 벗어나는 동작을 할 때가 있습니다.
여러 가지 원인 중 하나가, 컴파일러가 최적화를 하면서 코드를 재배치를 하다가 발생합니다.

이번 포스팅에서는 GCC의 최적화 레벨을 각각의 함수에 지정하는 방법을 소개합니다.
결론부터 말씀을 드리면 함수의 선언부에 아래 키워드를 추가하면 됩니다.

    * __attribute__((optimize("O0")))

예제 코드와 함께 ARM-GCC 최적화에 대해 알아봅시다.

ARM-GCC가 최적화하는 코드 예시

먼저 샘플 코드를 소개합니다.
void add_cal_func(void)
{
unsigned int a, b, c;
a = 7;
b = 3;

c = a + b;
printk("c = %u \n", c);
}

이 함수의 의도는 지역 변수를 3개 선언한 다음에, a에는 7, b에는 3을 저장합니다.
이어서 a와 b를 더한 값을 c에 저장합니다.

위와 같이 코드를 작성한 후 컴파일을 한 후 어셈블리 코드를 보겠습니다.

01 ffffff800859e744 <add_cal_func_02>:
02
03    a = 7;
04    b = 3;
05
06    c = a + b;
07    printk("c = %u \n", c);
08 ffffff800859e744:   b0001e40    adrp    x0, ffffff8008967000 <raspberrypi_firmware_pll_divider_clk_ops+0x30>
09 ffffff800859e748:   52800141    mov w1, #0xa                    // #10
10 ffffff800859e74c:   9114c000    add x0, x0, #0x530
11 ffffff800859e750:   17ee57db    b   ffffff80081346bc <printk>

어셈블리 코드를 보니 C 코드와 달리 코드가 구성돼 있습니다.

a와 b로 선언한 지역 변수에 7과 3을 저장하는 코드는 사라졌고, 다음 09번째 줄과 같이
x1 레지스터에 0xa를 더합니다.

09 ffffff800859e748:   52800141    mov w1, #0xa                    // #10

이어지는 11번째 줄에서 printk의 2번째 인자로 x1 레지스터를 전달합니다.

11 ffffff800859e750:   17ee57db    b   ffffff80081346bc <printk>

이처럼 코드가 바뀐 이유는, ARM-GCC 컴파일러가 똑똑하게  03~06번째 줄을 실행하면 어짜피 결과가 10이니 이를 계산해 버린 것입니다.
이번에는 C로 의도한 코드와 ARM-GCC가 최적화 과정에 수정한 코드를 비교해서 보겠습니다.

< 의도한 코드 >
03    a = 7;
04    b = 3;
05
06    c = a + b;

< ARM-GCC 컴파일러가 수정한 코드 >
03    a = 10;

이렇게 ARM-GCC 컴파일러는 C 코드를 보고 최적화를 수행하는데, 컴파일 과정에서 최적화 레벨을 지정할 수 있습니다.
에를 든 코드는 최적화 레벨이 O2로 적용해 ARM-GCC가 컴파일을 한 것입니다.

위에서 든 예시는 ARM-GCC 컴파일러가 최적화를 해도 문제가 될 만한 부분은 없습니다.
ARM-GCC 컴파일러가 최적화를 해줘 코드 사이드가 줄어드니 고맙다고 말해야 겠죠.

최적화가 되면 안되는 함수 예시

그런데, ARM-GCC 컴파일러에 의해 최적화가 되면 안되는 상황이 있습니다.

디바이스를 초기화하는 다음 함수를 예로 듭시다.

unsigned int *device_init_mem;
void device_init(void)
{
unsigned int a, b, c;
device_init_mem = (unsigned int*)0xC000D000;
a = 7;
*device_init_mem  = a;

b = 3;
*device_init_mem  = b;

mdelay(10);
}

만약 ARM-GCC 컴파일러의 최적화 레벨이 O2로 적용됐다면, 아래 코드는 아예 삭제해버리고, 

a = 7;
*device_init_mem  = a;

다음 코드만 실행할 수도 있습니다.

b = 3;
*device_init_mem  = b;

혹은 아예 우리가 전혀 의도하지 않는 코드를 만들어 낼 수도 있죠.

특정 함수에만 최적화 레벨을 지정하기

이 상황에서 특정 함수만 ARM-GCC 최적화 레벨을 적용해 컴파일하는 방법이 있습니다.
다음과 같은 키워드를 함수에 선언하면 됩니다.

    * __attribute__((optimize("O0")))

이번에는 add_cal_func() 함수만 최적화 레벨을 적용하지 않고 컴파일을 해봅시다.

void __attribute__((optimize("O0"))) add_cal_func(void)
{
unsigned int a, b, c;
a = 7;
b = 3;

c = a + b;
printk("c = %u \n", c);
}

'__attribute__((optimize("O0")))' 키워드를 함수의 선언부에 추가한 후 컴파일한 결과, 어셈블리 코드는 다음과 같습니다.

01 void __attribute__((optimize("O0"))) add_cal_func(void)
02 {
03 ffffff800859e6ec:   f81e0ffe    str x30, [sp,#-32]!
04    unsigned int a, b, c;
05
06    a = 7;
07 ffffff800859e6f0:   528000e0    mov w0, #0x7                    // #7
08 ffffff800859e6f4:   b90017e0    str w0, [sp,#20]
09    b = 3;
10 ffffff800859e6f8:   52800060    mov w0, #0x3                    // #3
11 ffffff800859e6fc:   b9001be0    str w0, [sp,#24]
12
13    c = a + b;
14 ffffff800859e700:   b94017e1    ldr w1, [sp,#20]
15 ffffff800859e704:   b9401be0    ldr w0, [sp,#24]
16 ffffff800859e708:   0b000020    add w0, w1, w0
17 ffffff800859e70c:   b9001fe0    str w0, [sp,#28]
18    printk("c = %u \n", c);
19 ffffff800859e710:   b0001e40    adrp    x0, ffffff8008967000 <raspberrypi_firmware_pll_divider_clk_ops+0x30>
20 ffffff800859e714:   9114c000    add x0, x0, #0x530
21 ffffff800859e718:   b9401fe1    ldr w1, [sp,#28]
22 ffffff800859e71c:   97ee57e8    bl  ffffff80081346bc <printk>

어셈블리 명령어가 02 최적화 레벨을 적용할 때에 비해 굉장히 많이 늘어났습니다.
코드를 잠깐 보니, 클래식 피아니스트가 '악보에 있는 음표를 그대로 놓치지 않고 연주한다'라고 말하듯,
의도한 C 코드를 그대로 구현한 어셈블리 명령어가 보입니다.

어셈블리 명령어를 차근차근 보겠습니다.
먼저 06~08번째 줄입니다.

06    a = 7;
07 ffffff800859e6f0:   528000e0    mov w0, #0x7                    // #7
08 ffffff800859e6f4:   b90017e0    str w0, [sp,#20]

07번째 줄에서 x0 레지스터에 7을 저장하고, 08번째 줄은 스택 주소 +20 메모리에
x0 레지스터를 저장합니다.

이어서 09~11번째 줄을 분석합니다.

09    b = 3;
10 ffffff800859e6f8:   52800060    mov w0, #0x3                    // #3
11 ffffff800859e6fc:   b9001be0    str w0, [sp,#24]

10번째 줄에서 x0 레지스터에 3을 저장하고, 11번째 줄은 스택 주소 +24 메모리에
x0 레지스터를 저장합니다.

이 동작을 다음 그림으로 나타낼 수 있습니다.


스택 주소       값
----------------------
...
+ 0x20          7 (a) 
+ 0x24          3 (b)


다음으로 14~15번째 줄을 보겠습니다.

14 ffffff800859e700:   b94017e1    ldr w1, [sp,#20]
15 ffffff800859e704:   b9401be0    ldr w0, [sp,#24]

14번째 줄을 봅시다. 
'스택 주소 +20 메모리'에 저장된, a 변수가 저장한 값(7)을 x1 레지스터에 로딩합니다.
이 연산 결과 'x1 = 7'이 됩니다.

15번째 줄도 비슷한 명령어인데, 
스택 주소 +24 메모리'에 저장된, b 변수가 저장한 값(3)을 x0 레지스터에 로딩합니다.

마지막으로 16번째 줄을 분석하겠습니다.

13    c = a + b;
...
16 ffffff800859e708:   0b000020    add w0, w1, w0

x1(a = 7)과 x0(b = 3)을 더해서 x0에 저장합니다.
이 동작은 다음 수식으로 표현할 수 있습니다.

   * x0(c) = x0(a) + x1(b)

최적화 레벨 O1를 적용

이번에는 '__attribute__((optimize("O1")))' 키워드를 적용해 add_cal_func() 함수를 컴파일해보겠습니다.

void __attribute__((optimize("O1"))) add_cal_func(void)
{
unsigned int a, b, c;
a = 7;
b = 3;

c = a + b;
printk("c = %u \n", c);
}

컴파일을 한 후 디스어셈블리(어셈블리 코드 확인)를 한 결과는 다음과 같습니다.

void '__attribute__((optimize("O1")))' add_cal_func(void)
{
ffffff800859e728:   f81f0ffe    str x30, [sp,#-16]!

    a = 7;
    b = 3;

    c = a + b;
    printk("c = %u \n", c);
ffffff800859e72c:   52800141    mov w1, #0xa                    // #10
ffffff800859e730:   b0001e40    adrp    x0, ffffff8008967000 <raspberrypi_firmware_pll_divider_clk_ops+0x30>
ffffff800859e734:   9114c000    add x0, x0, #0x530
ffffff800859e738:   97ee57e1    bl  ffffff80081346bc <printk>

처음에 예시를 든 어셈블리 코드와 유사합니다.

정리

ARM 리눅스에서 최적화 옵션을 적용해 컴파일을 하면, 컴파일러가 너무 똑똑해서 원작자의 의도와 다르게 코드를 재배치하는 경우가 있습니다.
악보를 보고 그대로 연주를 해야 하는데, 컴파일러가 스스로 악보를 재해석을 하는 것이죠.

하지만, 코드 사이즈를 최적화하기 위해 O2 정도의 최적화 레벨을 적용하는 경우가 많습니다. 그런데 특정 함수는 최적화 레벨을 적용하면 안될 때가 있죠.

이런 상황에서 ARM-GCC 에서 제공하는 '__attribute__((optimize("O1")))' 키워드를 함수에 선언하시기 바랍니다.

---
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)
---

덧글

  • ㅇㅇ 2020/06/30 20:42 # 삭제 답글

    임베디드쪽 그중에서 bsp쪽 진로 생각하는 3학년입니다. 그냥 단순히 저자님 책과 라즈베리파이 사서 리눅스만 공부하고 있는데(1권 거의 끝나갑니다) 혹시 이쪽으로 돈받고 먹고사려면 뭘 더 하면 좋을까요? 혹시 회로도도 볼줄 알아야 하나요?
  • AustinKim 2020/07/01 10:43 #

    1부까지 읽으셨다니 대단하십니다.

    2부를 구입하셨다면, 1부를 읽고 난 후 2부까지 꼭 읽어주셨으면 합니다. 리눅스 커널에서 가장 중요한 이야기를 2부에서 하거든요.
    특히 11/12/13장에서 리눅스 시스템 프로그램으로 실습하는 내용이 많은데, 이 부분을 읽으시면 리눅스 시스템 프로그래밍을 실행하면
    커널 내부에서 어떻게 동작하는지 알 수 있어요.

    이 밖에 '리눅스 시스템 프로그래밍', 'ARM 프로세서'까지 공부하시면 더욱 좋습니다.

    하드웨어도 잘 배워두면 좋지만, 아래 내용을 충분히 숙지하시면 실무에서 업무를 배우시는데 큰 무리가 없으리라 봅니다.

    http://recipes.egloos.com/
    1) Hardware 꼴라쥬 (Collage) - 회로도 읽기

    컴퓨터 아나토미
    http://www.yes24.com/Product/Goods/7859338
  • 2020/07/01 10:43 # 답글 비공개

    비공개 덧글입니다.
댓글 입력 영역