[컴퓨터구조 요약 정리] 13. SW for SoC 2

Embedded June·2021년 5월 21일
0
post-thumbnail

13.1. Program build tools

image-20210520130410399

  1. 우리가 작성한 모든 소스코드는 컴파일러가 컴파일 해서 .s 확장자의 어셈블리 파일로 translate 한다.
  2. 어셈블리 파일은 어셈블러가 컴파일 해서 .o확장자의 오브젝트 파일로 translate 한다.
  3. 링커와 로더는 라이브러리 파일과 오브젝트 파일을 엮어서 타겟머신에서 실행 가능한 파일로 만든다.

리소스가 제한된 임베디드 디바이스에서 개발을 할 때는 컴파일 과정조차 오버헤드가 될 수 있기 때문에 성능과 효율을 최대한 끌어내고자 할 때는 일부 코드를 어셈블리어로 짜기도 한다.

13.2. Register Use Convention

image-20210520142219490

개발과정에서 사용하는 register는 대부분 범용으로 사용할 수 있지만, ARM에서는 RISC-V처럼 관례로 n번 register에 대한 사용처가 정해져있다. 반드시 따라야 하는 규칙은 아니지만, 자기 자신을 위한 코드가 아니라 남에게 보여줄 가능성도 있는 코드를 짠다는 점을 고려하면 이런 관례를 따르는 것이 좋다.

이전에도 다뤘지만, convention을 따라서 register를 사용하다보면 프로세서에 따라 다시 원래 값으로 변경해줘야 하는 경우가 있을 수 있다. 이것 역시 convention에서 정의하고 있는데, R0~R3는 굳이 다시 원래 값으로 변경해줄 필요가 없지만 R4~R11은 사용 후에는 다시 원래 값으로 restore 해줘야 한다.

이제 우리가 배워왔던 C가 어셈블리나 하드웨어에서는 어떤 식으로 작동하는지 한 번 알아보자.

13.2.1. Function

Load & store 아키텍처를 따르는 프로세서는 속도와 효율을 위해 최대한 모든 연산에 register를 사용하려 노력한다. 이런 노력은 함수에서도 잘 나타나는데, 함수를 call 하거나 arguments를 넘겨주는 과정에서 어떤 register를 사용하는지 알아보자.

함수의 argument 관련한 register는 convention에 따르면 R0 ~ R34개가 고작이다. 따라서 4-byte 변수 4개 또는 8-byte 변수 2개를 겨우 사용할 수 있기 때문에 이보다 큰 값을 사용하기 위해서는 stack을 사용하는 방법이 있다.

int foo(char a, int b, char c) {	// 1-byte a, c | 4-byte b
    int x[8];
    x[0] = a * b;
    x[c] = b;
    return a + b + c;
}

위와 같은 코드가 있을 때 ARM 어셈블리에서는 어떤 일이 발생하는지 공부해보자.

;;;101	int	foo(char a, int b, char c)
0000ba	b510	PUSH	{r4, lr}
0000bc	b088	SUB		sp, sp, #0x20
;;;102	int x[8]; 
  • 맨 처음에 함수를 call하게 되면 어셈블러는 자동으로 돌아올 주소 lrr4값을 stack에 저장한다.
  • 현재 함수에서 local variable로 int형 8칸 배열인 x를 사용하므로 stack pointer를 그만큼 올려줘야 한다.
    이때 이미 lrr4PUSH했으므로 총 2+8 = 10칸만큼 stack pointer를 옮겨줘야 하므로 0x20SUB해줘야 한다.
  • 다시 한 번 강조하지만, memory에서 stack은 위에서 아래로 향하므로 SUB 연산을 통해 stack pointer를 아래로 내려줘야 한다.
  • 함수에서 곱셈과 대입 연산을 수행하게 되면 함수를 종료하며 다음과 같은 과정을 수행한다.
;;;105 return a + b + c;
0000ca	1840	ADDS	r0, r0, r1
0000cc	1880	ADDS	r0, r0, r2
;;;106	}
0000ce	b008	ADD		sp, sp, #0x20
  • Return value를 계산하는 과정을 보이고 있다. r0는 return value를 위한 register임이 convention에 기재돼있다. r0r1r2를 더해서 return 해준다.
  • 이제 다시 원래 주소로 돌아가면서 local variable을 위해 stack에 할당한 공간을 반환해야 한다. 이 과정은 stack pointer를 아까 SUB해준 만큼 ADD함으로써 간단히 끝마칠 수 있다.

13.2.2. Conditions

가장 대표적인 조건문인 if-else 문이 어떻게 표현되는지도 알아보자. RISC-V를 공부하면서 이미 다뤘던 내용이지만, 복습한다는 생각으로 다시 보도록 하자.

if (x) y++;
else y--;

위와 같은 간단한 if-else 구조의 c 코드가 있다고 생각해보자. 이를 어셈블리어로 바꾸면,

;;;	if (x)
CMP		r1, #0
BEQ		Else
;;; y++;
ADDS		r2, r2, #1
B		Out

Else
;;; else y--;
SUBS		r2, r2, #1

Out

image-20210520145249684

  • CMP와 Branch 명령어를 사용해서 labeling 된 곳으로 분기를 옮기면서 if-else문을 구현하는 것을 확인할 수 있다.
  • 주의할 점은 CMP와 BEQ가 함께 사용된다는 점이다. RISC-V에서는 branch 명령어에서 CMP를 동시에 수행했지만, ARM에서는 CMP 명령어를 수행하면 APSR의 flag bit가 set 되고, 이 값을 근거로 branch를 수행한다는 점을 기억하자.
  • Switch-case 문도 위와 같은 방법으로 구현한다.

13.2.3. Loops

do { x += 2; } while (x < 20);
;;; do {
NOP

[Loop]
;;; x += 2;
ADDS	r1, r1, #2
;;; while (x < 20);
CMP	r1, #0x14
BCC	[Loop]		
while ( x > 10 ) { x = x + 1; }
;;; while (x > 10)
B	[Check]
[Plus]
;;; x = x + 1;
ADDS	r1, r1, #1
[Check]
CMP	r1, #0xa
BCC	[Plus]

while은 do-while과 달리 먼저 조건을 검사해주고 만족하지 않으면 뛰어넘어가기 위해 unconditional 하게 branch를 해주는 과정이 포함돼있다는 점이 특징이다.

for (i = 0; i < 10; ++i) { x += i; }
;;; for (i = 0; i < 10; ++i) {
MOVS	r3, #0
B	[Check]

[Plus]
;;; x += i; }
ADDS	r1, r1, r3
ADDS	r3, r3, #1

[Check]
CMP	r3, #0xa
BCC	[Plus]

for문도 마찬가지로 진입조건을 검사하기 위한 unconditional branch가 포함돼있다. ir3가 담당한다.


profile
임베디드 시스템 공학자를 지망하는 컴퓨터공학+전자공학 복수전공 학부생입니다. 타인의 피드백을 수용하고 숙고하고 대응하며 자극과 반응 사이의 간격을 늘리며 스스로 반응을 컨트롤 할 수 있는 주도적인 사람이 되는 것이 저의 20대의 목표입니다.

0개의 댓글