사실, 이 글은 딱히 ARM architecture에 국한 된 것은 아니다.


"Alignment가 무엇인가" 대한 글인데.. 살짝 요약하기 전에 바로 본론으로 들어가보도록 한다.



컴퓨터라는 놈은 특히 ARM architecture에서는 무언가를 실행하기 위해서, CPU 바로 옆에 있는 레지스터와 메모리 사이에 읽고 쓰는

연산이 굉장히 많다. 이때 주의깊게 봐야 할 점은 메모리에서, 어떤 주소에서 얼마 만큼 읽어 올 것인가가 주된 내용이다.


아래 그림에서 각 메모리 주소들은 주소를 나타내고 각 주소마다 1byte 만큼을 의미한다.



각각의 예시를 통해서 개발자가 생각하는 컴퓨터의 예상 행동과 실제 컴퓨터의 행동을 비교해보도록 해보자
먼저, 개발자가 생각하는 컴퓨터의 예상 행동은

  • READ 주소0 부터 1byte: 아래 색칠 되어있는 0 부분만 읽어오는 것

  • READ 주소3 부터 2byte: 아래 색칠되어있는 3 그리고 4를 읽어오는 것
  • READ 주소3 부터 4byte: 아래 색칠되어있는 3, 4, 5 그리고 6를 읽어오는 것


하지만, 실제 컴퓨터의 동작은 프로그래머가 생각하는 방식으로 동작하지 않고 읽거나 쓸 때 Chunk 단위로 동작하는데 이 단위는 CPU에 따라 다르다. 예를 들어 32bit architecutre에서는 32bit chunk 단위로 움직이고, 64bit architecture에서는 64bit chunk 단위로 움직이게 된다. 즉,

  • READ 주소0 부터 1byte: 0 번째 byte를 읽기 위해서 0,1,2 그리고 3번째 byte를 모두 읽고, 그 중 0번 째 byte 값만 다시 OR 연산을 통해
                                             해당 0 번째 byte만을 결과로 갖는다.
  • READ 주소3 부터 2byte: 3 번째 부터 2 byte를 읽기 위해서 0,1,2,3 그리고 4,5,6,7 번째 byte를 모두 읽고, 그 중 3번 째, 4번째 byte 값만 다시
                                            OR 연산을 통해 결과로 갖는다
  • READ 주소3 부터 4byte: 3 번째 부터 4 byte를 읽기 위해서 0,1,2,3 그리고 4,5,6,7 번째 byte를 모두 읽고, 그 중 3,4,5,6번 째 byte 값만 다시 OR
                                             연산을 통해 결과로 갖는다.

 이처럼, 프로그래머가 읽고 싶은 번째부터 몇 개의 byte만을 읽기 위해서 실제로는 chunk 단위로 읽어 들여야 하다 보니 생각치 못한 오버헤드가 생긴다. 그래서 CPU가 자체적으로 이렇게 오버헤드가 생기지 않게 하기 위해서, Alignment check를 통해서 자체적으로 오버헤드를 줄일 수 있게 해 놓았다.(읽고자 하는 단위와, 주소를 통해서 Alignment를 체크한다.)




 Memory Access

 Alignment (8bit)

 Alignment (16bit)

 Alignment (32bit)

 Alignment (64bit)

 0x0000_0000

 Aligned

 Aligned

 Aligned

 Aligned

 0x0000_0001

 Aligned

 Non-Aligned

 Non-Aligned

 Non-Aligned

 0x0000_0002

 Aligned

 Aligned

 Non-Aligned

 Non-Aligned

 0x0000_0003

 Aligned

 Non-Aligned

 Non-Aligned

 Non-Aligned

 0x0000_0004

 Aligned

 Aligned

 Aligned

 Non-Aligned

 0x0000_0005

 Aligned

 Non-Aligned

 Non-Aligned

 Non-Aligned

 0x0000_0006

 Aligned

 Aligned

 Non-Aligned

 Non-Aligned

 0x0000_0007

 Aligned

 Non-Aligned

 Non-Aligned

Non-Aligned

 0x0000_0008

 Aligned

 Aligned

 Aligned

 Aligned




위에 테이블과 같이 해당 메모리는 접근 할 때, 위에 표를 참조하여 Alignment를 체크하고 올바르지 않은 접근을 하게 되면 Alignment Fault를 발생시킨다.



















'전공공부 > ARM' 카테고리의 다른 글

ARM Relocations (adrp)  (0) 2020.10.18
[링커스크립트] 지시어 ENTRY  (0) 2019.02.12
GICv3 LPI, ITS  (0) 2018.07.18
interrupt status - level sensitive  (0) 2018.07.17
ARMv8 HPPIR/IAR, AHPPIR/AIAR  (0) 2018.03.27
이 글에서는 리눅스 커널이 빌드 된 후, 커널 이미지의 헤더에 대해서 알아보도록 한다.

임베디드 환경(통상 ARM Architecture)에서,리눅스 커널과 같은 운영체제는 메모리에 로드가 된 후 실행된다.
그렇다면 여기서!, 드는 질문 2가지?!
  • 메모리에 적재시켜주는데... 메모리 몇 번지에 로드시켜야 하는가?
  • 커널 이미지는 누가 메모리에 로드시켜 주는가?
첫 번째 질문에 대한 대답을 먼저 하면..

 리눅스 커널은 PIC(Position Independent Code)라고 한다. 말 그대로 어느 메모리에 올려도 자기가 알아서 원하는 메모리 주소로 Copy해서 동작할 줄 아는 소프트웨어이다.(물론, 프로그램이 정상적으로 동작 할 수 있는 메모리여야 한다)

 결론적으로, 그래서 딱히 어느 메모리에 로드웨어야 한다 이런것은 없다.얕지만 알고 있는 경험에 의하면, 이미지를 로드하는 메모리 주소가 Align(이 부분에 대해서는 나중에 글을 올릴 예정이다)만 되어있다면 어느 주소에도 동작하는 것을 확인했었다. arm32 에서는 zImage가 원하는 주소에 kernel image를 압축해제 했었다.

두 번째 질문에 대한 대답을 하면..

 커널을 메모리에 올려주는 놈은 부트로더(Bootloader)라고 불리는 소프트웨어가 해준다. 보드를 몇 번 실행해봤다면 흔히 들어봤을 법한 U-boot, LK(Little Kernel), UEFI 이런 놈들이 부트로더에 해당된다.

부트로더도 크게는 First-bootloader, Second-boorloader 로 나눠진다. 간랸히 설명해보면 First-Bootloader는 ATF(ARM Trusted Firmware)에 해당하는 보드를 만드는 vendor사에서 만드는 펌웨어로 보드를 부팅시키는 놈이다.(사실 하나의 소프트웨어가 아니라 여러개로 나뉘어져있다.)
Second-Booloader가 위에서 언급한 부트로더들이다.

이로써 두 개의 질문과 답을 마치고 이어서 이 글의 주제로 넘어와 보면,(지금부터 말하는 부트로더는 Second-Bootloader 이다.)
부트로더는 크게 2개의 역할로 볼 수 있다.(물론 세부적으로는 많은 일들을 하지만 )
첫 번째는 보드의 디바이스, 메모리 초기화. 두 번째는 운영체제 메모리에 적재이다. 그럼 이 때 부트로더는 내가 로드하고 있는 이 운영체제가 어떠한 운영체제인지, 어떠한 프로토콜(운영체제마다 다르겠지만 리눅스에서는 레지스터에 값을 전달해주는 정도를 뜻한다)을 따라주어야 하는지를 먼저 알아야 한다. 그 때 하는 것이 리눅스커널 이미지를 읽어보는 것이다. (전부다는 아니고 처음 시작 부분을 읽어본다)

그럼 리눅스 커널은 부트로더에게 "내가 커널이미지야!" 라고 말해주는 역할로 헤더를 만들어놓았다. Header에 대한 자세한 설명은 참조[1]에 있지만 간략히 설명해보자면 아래와 같이 생겼다.

u32 code0;                            /* Executable code */

u32 code1;                            /* Executable code */

u64 text_offset;                     /* Image load offset, little endian */

u64 image_size;                     /* Effective Image size, little endian */

u64 flags;                               /* kernel flags, little endian */

u64 res2  = 0;                        /* reserved */

u64 res3  = 0;                        /* reserved */

u64 res4  = 0;                        /* reserved */

u32 magic = 0x644d5241;   /* Magic number, little endian, "ARM\x64" */

u32 res5;                               /* reserved (used for PE COFF offset) */


그리고 실제로 커널의 코드를 통해서 이 헤더가 구현되어있는 모습을 보자면, (단 EFI가 아니라 Uboot를 위한 헤더라고 가정한다)


b   stext                                      // branch to kernel start, magic

.long   0                                     // reserved

le64sym _kernel_offset_le       // Image load offset from start of RAM, little-endian

le64sym _kernel_size_le          // Effective size of kernel image, little-endian

le64sym _kernel_flags_le        // Informative flags, little-endian

.quad   0                                            // reserved

.quad   0                                            // reserved

.quad   0                                            // reserved

.ascii  ARM64_IMAGE_MAGIC       // Magic number

.long   0                                             // reserved

                                         <linux/arch/arm64/kernel/head.S>


이 헤더는 리눅스 커널 이미지의 처음 시작 64byte 값이다. 부트로더 코드를 읽어보지는 않았지만, 일단 ARM64_IMAGE_MAGIC("ARM\x64") String값을 읽고 일단 리눅스 커널이라는 것을 알아낸 후 사이즈에 대한 정보를 통해서 메모리에 적재하는 것으로 추정된다. 그리고 메모리에 올린 후 제어권을 리눅스 커널로 넘겨주어야 하는데 이 때 아래와 같은 값을 각각 레지스터에 써서 넘겨준다. (이 때, 제어권을 넘겨준다는 말은 PC값을 이미지의 처음 주소로 assign한다는 의미가 주된 의미지만 필요시 CPU mode를 바꿔줌으로써 넘겨준다.)


- Primary CPU general-purpose register settings
  x0 = physical address of device tree blob (dtb) in system RAM.
  x1 = 0 (reserved for future use)
  x2 = 0 (reserved for future use)
  x3 = 0 (reserved for future use)

(DTB가 무엇인지 대한 설명은 다른 글에서 하도록 한다...하 씨발 쓸거 개 많네)

이 글에서는 리눅스 커널 이미지에는 헤더라는게 왜 있어야 하는지에 대한 배경설명과, 헤더의 모양은 어떠한지 그리고 커널의 실제 헤더의 값은 어떻게 되어있는지에 대해서 보았다.







참조:
[1] linux/Documentation/arm64/booting.txt


프로그래머가 코딩을 처음 배울 때, 아주아주 처음 배우면 먼저 해보는 Helloworld!.


#include<stdio.h>


int main(int argc, char **argv)

{

    printf("Hello World\n");

    return 0;


이 코드를 작성해서 터미널에 

gcc helloworld.c -o helloworld 라고 명령어를 치거나, Visual-Studio를 통해서 Run 버튼을 누르면, 결과창에 나타나는 것은 Hello World라는 한 줄이다.


왜 인지는 모르지만, main 이라는 함수를 실행하는거 보니, 이 프로그램은 main 이라는 함수가 시작지점 즉, Entry-point 인가 보다 하면서 공부를 시작했던게 기억난다. 시간이 지나고 보니 내가 했던 생각은 맞기도, 틀리기도 한 내용이다. 엄밀히 말하면 틀리기 보다는 똑똑한 운영체제가 나 대신에 Entry-point를 main함수로 지정해 준 것이라는걸(운영체제가 main함수를 엔트리 포인트로 하고 있는, 디폴트 링커스크립트를 제공해준다는 걸) 나중에 머리가 조금 크고 나니 알게 되었다.


프로그램을 작성하려면 내 코드를 기계어로 바꿔주는 컴파일러에게 "내가 만든 이 코드들을 여기부터 시작해야되!" 라는 힌트를 제공해주어야 한다. 사실, Entry-point 말고도 여러가지 힌트들을 제공해주어야 하는데 링커스크립트를 통해서 제공해준다.


바로 위 문장을 보고나면 아래와 같은 질문들이 생긴다.

  • 링커스크립트(Linker-Script)가 뭔대?!
  • 어떠한 힌트들을 제공해 주는데?
  • Entry-point에 대한 힌트는 어떻게 제공하는데?
모든 질문에 대한 답은 나중에 다른 글을 통해서 알게되겠지만, 이 글에서는 마지막 질문에 대해서만 대답해보려 한다.
링크를 눌러 한 번 살펴보면, ENTRY라는 명령어 또는 지시어 라고 불리는 함수 같이 생긴 놈을 호출하는 형식으로 내 프로그램이 처음으로 시작되는 지점을 컴파일러에게 힌트로 제공하는 셈이다. 컴파일러는 내가 만든 코드들을 기계어로 바꿔주는 역할을 하고, 이러한 컴파일러에게 
"너가 만든 기계어 중에서 제일 먼저 시작되어야 할 기계어는 이거야" 라고 말을 해주어야 하는 셈이다.

리눅스 커널의 링커스크립트를 살펴보면,

<linux/arch/arm64/kernel/vmlinux.lds.S>


이렇게 _text라는 심볼을 리눅스의 처음 시작 지점으로 할게! 라고 컴파일러에게 정보를 제공해주고 있다.


이처럼 링커스크립트에서 ENTRY라는 지시어는 프로그램의 시작지점 즉 엔트리 포인트를 지정해주는 지시어! 이다.




 










'전공공부 > ARM' 카테고리의 다른 글

ARM Relocations (adrp)  (0) 2020.10.18
[ARM] Alignment가 무엇인가?!  (0) 2019.02.17
GICv3 LPI, ITS  (0) 2018.07.18
interrupt status - level sensitive  (0) 2018.07.17
ARMv8 HPPIR/IAR, AHPPIR/AIAR  (0) 2018.03.27

리눅스 커널을 Line-by-line으로 분석하려면, 특히 리눅스 커널 이미지가 처음부터 실행되는 모습을 보고자 한다면!

몇 가지 배경 지식을 알고 있어야 한다.


이 간단한 배경 지식들을 알게 되었거나 링크를 눌러보면서 읽고 알게 되었다면 이제부터!

Linux를 Line-by-line으로 보기 위해서 리눅스의(ARM architecture의 arm64) 처음 시작점부터 알아보려고한다. 



1

2

3

4

5

6

7

8

 ENTRY(_text)


    ...

. = KIMAGE_VADDR + TEXT_OFFSET;

.head.text : {
    _text = .;
    HEAD_TEXT
}


첫 번째 줄이 바로 위에 설명한 ENTRY 지시어이다. 이는 "지금 컴파일의 결과물로 나오는 프로그램 이미지의 시작은 _text 라는 symbol이다." 라는 뜻이며 이러한 정보를 컴파일러에게 힌트로 제공해주는 것이다.

다섯 번 째 줄은 컴파일의 결과물 즉, 이 프로그램 이미지는 ".head.text" 라는 섹션을 포함하는데 이 섹션에는 이러한 내용들이 들어가 있다.
이 section에서 _text 심볼의 값을 assign해주고 있는데 그 값은 "." 즉, 위에 KIMAGE_VADDR + TEXT_OFFSET에 해당하는 값이다.
그리고 7 번째 줄에 있는 HEAD_TEXT라는 매크로 살펴보면

#define HEAD_TEXT  KEEP(*(.head.text)) 


이와 같이 정의되어있다. Linking 할 모든 Object 파일들의 "head.text" section을 모아 여기에 링킹 하겠다 하는 의미이다.

밑에서 head.S 내용에서 보겠지만 head.S의 일부 코드도 이 section에 포함되어있는 코드들이다.


그럼 이제 이러한 배경 지식을 갖고 리눅스의 처음(head.S) 으로 가보겠다.


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

32

     __HEAD
_head:
    /*
     * DO NOT MODIFY. Image header expected by Linux boot-loaders.
     */
#ifdef CONFIG_EFI
    /*
     * This add instruction has no meaningful effect except that
     * its opcode forms the magic "MZ" signature required by UEFI.
     */
    add    x13, x18, #0x16
    b    stext
#else
    b    stext                // branch to kernel start, magic
    .long    0                // reserved
#endif
    le64sym    _kernel_offset_le        // Image load offset from start of RAM, little-endian
    le64sym    _kernel_size_le            // Effective size of kernel image, little-endian
    le64sym    _kernel_flags_le        // Informative flags, little-endian
    .quad    0                // reserved
    .quad    0                // reserved
    .quad    0                // reserved
    .ascii    ARM64_IMAGE_MAGIC        // Magic number
#ifdef CONFIG_EFI
    .long    pe_header - _head        // Offset to the PE header.

pe_header:
    __EFI_PE_HEADER
#else
    .long    0                // reserved
#endif



첫 번째 줄에 _HEAD라는 매크로는 아래와 같이 정의되어있고, 위에서 설명한 것 처럼 _text 실볼에 해당하는 section이다.

#define __HEAD        .section    ".head.text","ax" 


EFI를 사용하지 않고 있다고 가정하고있으므로, Image의 첫번째 명령어는 14번째 줄이 될 것이다.

부트로더가 리눅스를 실행시키면 14번 째 줄 명령어를 처음으로 실행하게 되고, 즉 stext 함수를 실행하게 된다.


다음부터는 stext 함수에 대해서 보겠다.







따로 만들어야 하는 문서: 링커스크립트, ELF, 섹션

LPI: Locality-specific Peripheral Interrupt

ITS: Interrupt Translation Service


GICv3에는 기존 GICv2와는 다르게 위 내용들이 추가되었다.




출처: http://infocenter.arm.com/help/topic/com.arm.doc.dai0492b/GICv3_Software_Overview_Official_Release_B.pdf

'전공공부 > ARM' 카테고리의 다른 글

ARM Relocations (adrp)  (0) 2020.10.18
[ARM] Alignment가 무엇인가?!  (0) 2019.02.17
[링커스크립트] 지시어 ENTRY  (0) 2019.02.12
interrupt status - level sensitive  (0) 2018.07.17
ARMv8 HPPIR/IAR, AHPPIR/AIAR  (0) 2018.03.27


level sensitive only


  • Signaling from device to GIC: interrupt state: Pending
  • Sending the signal from GIC to CPU: Pending
  • Acknowledge (ACK) the interrupt through CPU interface: Active & Pending
  • Operating System reprogram the device and device switch from high to low: Active
  • CPU deactivate(write corresponding interrupt to EOI) the interrupt: Active to Inactive


'전공공부 > ARM' 카테고리의 다른 글

ARM Relocations (adrp)  (0) 2020.10.18
[ARM] Alignment가 무엇인가?!  (0) 2019.02.17
[링커스크립트] 지시어 ENTRY  (0) 2019.02.12
GICv3 LPI, ITS  (0) 2018.07.18
ARMv8 HPPIR/IAR, AHPPIR/AIAR  (0) 2018.03.27

 

 Non-secure

Secure 

 HPPIR/IAR

Grp1 ID / 1023 

Grp0 ID 

Grp1 pending: 1022,

No pending; 1023 

 AHPPIR/AIAR

 -

Grp1 ID 

No Pending: 1023 


'전공공부 > ARM' 카테고리의 다른 글

ARM Relocations (adrp)  (0) 2020.10.18
[ARM] Alignment가 무엇인가?!  (0) 2019.02.17
[링커스크립트] 지시어 ENTRY  (0) 2019.02.12
GICv3 LPI, ITS  (0) 2018.07.18
interrupt status - level sensitive  (0) 2018.07.17

개인적으로 공부하고 싶은 내용들에 대해서 List화 하고, 정리해 나갈 예정

[ x ]  : 시작 안함

[ - ]  : 진행 중

[ O ] : 완료



리눅스

    • [ - ] Linux Code Review line by line
    • [ x ] XEN on ARM Code Review line by line
    • [ x ] cgroup
    • [ x ] subsys


ARM

    • [ x ] ARMv8
    • [ x ] GICv3 & 4
    • [ x ] PMIC
    • [ x ] PSCI
    • [ x ] SMCCC
    • [ x ] ARM-EABI (Embedded Application Binary Interface)

Hardware

    • [ x ] 전기는 왜 흐르나
    • [ x ] flash memory
    • [ x ] EERPROM
    • [ x ] MMC
    • [ x ] eMMC
    • [ x ] UART (pl001x)


ETC

  • [ x ] ARM assembly .macro vs FUNC

  • [ x ] UEFI 사용해보기


git 계정 casionwoo@github.com 에서 casionwoo@gmail.com으로 바꾸기


git filter-branch --env-filter '

OLD_EMAIL="casionwoo@github.com"

CORRECT_NAME="casionwoo"

CORRECT_EMAIL="casionwoo@gmail.com"

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]

then

    export GIT_COMMITTER_NAME="$CORRECT_NAME"

    export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"

fi

if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]

then

    export GIT_AUTHOR_NAME="$CORRECT_NAME"

    export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"

fi

' --tag-name-filter cat -- --branches --tags


git log -p -2

+ Recent posts