실행속도는 32비트 명령어셋이 더 빠르다(pipeline 구성이 용이하기 때문이다). 하지만 그만큼 메모리를 더 많이 먹는다.
모든 ARM명령어는 조건부 실행이 가능하다는 특징이 있다.
명령어가 32비트인데 32비트 안에 32비트 주소를 넣는 것이 불가능하기 때문에 Load/Stroe와 같은 메모리 참조명령이나 Branch 명령에서 모두 상대주소 지정방식을 사용하게 된다. (<->적치 데이터(ex. immediate 상수): 코드 안에 상수값을 직접 사용하는 것)
인텔 명령어셋에서 jump에 해당하는 것이 B(branch)이다.(B는 점프하고 돌아오지 않는다. BL은 점프하고 돌아온다.)
커널이나 디바이스드라이버에서 사용할 시스템 콜 함수가 컴파일 되고 나면 전부 SWI명령으로 바뀐다. 사용자영역에서 커널영역으로 진입할 수 있는 유일한 통로가 SWI 명령어이다. 그래서 앞으로 자주 보게 될 것이다.
Thumb state와 ARM state를 변경할 때 BX 명령어를 사용한다.
32비트 명령어셋과 16비트 명령어셋은 하나의 ARM머신에서 서로 Interwork하며 같이 사용된다.
우리가 STM 펌웨어 코딩을 하면 그 프로그램은 전부 supervisor mode에서 돌아간다고 보면 된다. 거기서 RTOS가 올라가면 User mode에서 돌아가다가 시스템 콜 함수를 통해서 커널에 접근하게 되는 것이다.
ReadOnly인데 write를 하려고 한다거나, 잘못된 메모리 접근을 하려 할 때 Abort Mode로 들어가게 된다.
리눅스에서 프로그래밍 하다보면 흔하게 만나게 되는 것이 segmentation fault이다. 리눅스는 메모리를 페이지 단위로 나눠서 접근하는데, 우리가 메모리 주소로 접근을 잘못할 때 보통 발생하게 된다.(포인터 연산을 하거나 할 때 발생할 수 있다.)
C++로 가면서 포인터 대신에 참조변수를 많이 사용하게 된다. 원리적으로는 참조변수도 포인터이다. 잘못된 포인터 연산을 방지하기 위해서 쓰곤 한다.
~/pi_bsp/u-boot/arch/arm/lib$ vi vectors.S
vectors.S 파일은 U-Boot 부트로더에서 ARM 아키텍처에서 실행되는 초기 벡터 테이블을 정의하는 어셈블리어 코드 파일입니다. 이 파일은 부트로더가 실행되면서 가장 먼저 로드되는 코드 중 하나이며, 시스템의 초기화 및 전원 켜기 후 실행되는 코드에 대한 진입점을 정의합니다.
25번라인의 "b reset" 명령을 통해 0번째 주소로 이동하게 된다. 그 과정은 하드웨어적으로 리셋 핀을 통해 이루어지게 된다. 저기서 이동하게 된 reset이라는 라벨은 start.S에 구현되어 있었다.
그 아래로 28번째 라인부터 0x04, 0x08, 0x0C, 0x10, ... 이런 순으로 주소가 할당되어 program counter에 들어가면서 지정된 라벨이 실행되게 된다.
시스템 레지스터는 보통 GPIO나 peripharal을 설정하기 위해 사용되는 레지스터이고, 여기서 볼 것은 CPU 연산장치에서 사용되는 범용 레지스터이다.
ARM에서 실제로 사용하는 레지스터는 16개가 맞다. 하지만 위에서 살펴보았던 각각의 동작모드마다 shadow로 사용하는 레지스터가 추가적으로 있다. 그래서 ARM에는 32비트 길이의 레지스터가 총 37개가 있다.
ARM에서 r15 레지스터는 PC용으로 고정이다.
CPSR은 코어가 1개라면 1개이고, 4개이면 4개가 된다. 당연한 것이다. 하나의 코어는 하나의 프로세스를 실행할 수 있기 때문이다.
operating 모드가 변경되면 현재 레지스터 값들을 전부 stack에 집어넣는다.
기본적으로 함수가 호출될 때마다 전달한 인자와 정의한 지역(자동) 변수가 높은 주소부터 낮은 주소의 방향으로 차례대로 저장되는 구조이다. 이외에도 다른 함수를 호출할 때 복귀할 주소(다음 실행할 명령어의 주소), 프레임 포인터 및 보존되는 레지스터들이 스택에 저장된다. 스택 포인터는 함수 호출 시작부터 피호출 프로그램이 실행되는 단계 차례대로 저장되는 값들을 저장하기 위해 현 시점에서 저장할 메모리의 위치를 가리킨다.
스택 프레임에 저장되는 값
- 복귀 주소
- 호출자 루틴의 프레임 포인터
- 사용하던 보존 레지스터
- 피호출자에 전달하는 인자
- 피호출자에서 사용되는 지역 변수들
지역변수를 하나라도 쓰면 스택이라는 메모리 공간에 할당되게 된다. 그리고 스택포인터는 낮은주소에서 높은주소 방향으로 다음 변수가 들어올 공간을 가리키고 있게 된다. 스택이라는 메모리공간은 스택포인터가 자동으로 관리해주기 때문에 auto라고 부르기도 한다.
힙 메모리 영역에서 직접 malloc()과 free()를 해줘야 하는 것과 대조적인 부분이다.
(인프런 어셈블리어배우기 강좌에서 스택포인터 부분 다시 한번 봐야겠다.)
ARM 아키텍처 자체는 Big/Little Endian을 모두 지원한다.
하지만 일반적으로 제조사에서 ARM으로 설계한 범용 MCU는 생산된 순간 엔디안을 변경할 수 없는 방식으로 생산한다.(ex. STM32)
주소가 4씩 증가하는 방식으로 메모리 데이터를 access하는 것이 일반적이다.
기본적으로 예외처리 벡터 테이블이라 함은 발생 가능한 각각의 Excetpion에 대하여 Vector를 정의해 놓은 테이블이다. ARM은 기본적으로 0x00000000에 Vector Table을 둔다.
이와 비슷하게 ARM은 NVIC(Nested Vector Interrupt Table)라는 중첩벡터인터럽트컨트롤러를 통해서 인터럽트를 처리하는 기능을 지원하는 것이다.(인터럽트 우선순위도 변경가능)
너무나 당연한 처리 플로우이다. 차분하게 따라가면 뭔말인지 다 이해가 되는 쉬운 내용이다. 다만 여기서 중요한 것은 이 처리과정이 전부 "하드웨어적으로" 일어난다는 것이다. 그래서 예외처리는 굉장히 빠른 속도로 일어난다.
이러한 예외처리를 실시하기 위해 레지스터를 사용하는 순서를 그림으로 나타내면 아래와 같다.
ARM의 획기적인 16비트 Thub-2 명령어가 퍼포먼스 측면에서 32비트 ARM 명령어를 따라잡았다. 그래서 요즘은 컴파일 될 때 알아서 16비트 명령어와 32비트 명령어를 섞어서 최적의 상태로 컴파일 된다. 16비트 명령어와 32비트 명령어는 interwork한다.
수많은 명령어를 하나하나 다 보는 것은 의미가 없다.
우리가 작성한 코드에 대해 Disassembly 분석을 하면서 ARM 아키텍처의 어셈블리 명령어와 친해져보겠다.