포스트

5. Software 비네팅(Vinetting)

Embedded Recipes

목차


ⓐ Context와 AAPCS


context

  • Context
    • 현재 CPU에 대한 모든 정보. 즉 레지스터들의 현재 값들
    • 시스템이 동작하는데 필요한 모든 정보를 담고 있음
    • R0~R12는 계산용
    • R13 : Stack의 Top부분. 마지막으로 push한 데이터의 주소
    • R14 : Linked Register. 어디서 왔는지, 어디로 돌아가야하는지
    • PC : 현재 실행하고 있는 주소 정보
    • CPSR : 현재 CPU의 상태에 대한 주소
      1. Callee(호출된 함수)에서 Register 값을 변경하면 Caller 함수에서 Register 값이 변경되어 문제가 발생함
      2. 이를 방지하기 위해, Callee는 Caller로 복귀할 때, 원래의 환경을 그대로 복구해주어야함
      3. Scratch Register(R0-R3, R12)는 Callee가 마음대로 변경 및 수정, 훼손의 권리를 가지고 있음. 어느정도의 유연성
      4. Function Call의 경우, Scratch Register를 훼손하였으나, 다른 함수를 호출하지 않는 경우(Leaf 함수) Stack에 백업할 필요헚이, 돌아올 떄, MOV pc, lr을 이용해 곧바로 복구 가능
  • Exception 발생 순간, Function Call, Scheduler에 의한 Context Switching 모두 비슷함
  • RTOS에서의 Context Switching은 추후 다룰 예정

ⓑ Pointer와 Array는 소녀시대와 원더걸스 , 그리고 이중포인터


  • Pointer : 주소를 가리키는 word 크기의 자료 형
  • int *addres
    • pointer 형 자료형, address 자체의 크기는 32bit, address가 가리키는 자료형은 integer type
    • & :실제 메모리 주소를 확인하는 연산자.
  • EX1) char *text = "Recipes;
    • text는 문자 ‘R’이 저장되어있는 주소를 가리킴
    • text가 0x100이라면, 0x101은 e, 0x102는 c …
  • text[1]; ‘e’, *(text+1); 도 ‘e’를 가리킨다
    • 이는 컴파일러가 똑같이 해석하기 때문. Sugar Expression이라고 함
    • 개발자가 어떻게 하더라도 같은걸 가리킨다는 의미
  • array[번호]*(array주소 + 번호)와 같음
  • Ex2)
1
2
3
4
5
6
7
8
char *pointer;
char array[3];
array[0] = 0;
array[1] = 1;
array[2] = 2;

pointer = array;
pointer = &array[0];
  • 마지막 두 문장은 같은 의미
  • 왜 포인터를 사용하는가? 포인터 사용의 장점
    • 직접 주소를 가리킬 수 있기 때문에, Assembly와 유사함
    • High Level Language에서 각종 memory 주소에 Access할 수 있는것이 장점
    • Function Call을 할 때, Passing Argument로 주소를 직접 전달 가능
      • 대용량의 데이터의 전달을 간단히 주소 하나로 가능하게 해줌.
  • EX3) swap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int process(void)  
{  
	int a, b;  
	int a = 1;  
	int b = 2;  
	swap(&a, &b);  
}

void swap(int* a, int* b)  
{  
	int tmp;  
	tmp = *a;  
	*a = *b;  
	*b = tmp;  
	return;  
}
  • process함수는 a와 b의 주소를 swap 함수에 넘겨주고, swap 내부에서 알아서 주소를 가져다 값을 바꿈
  • 이중 포인터 Pointer를 가리키는 Pointer
    • 컴파일러에 따라 컴파일이 될수도 안될수도 있음
    • pointer 배열에서 가장 많이 사용됨
  • EX4) 이중 포인터
1
2
3
4
5
6
7
8
9
10
11
12
void gettag (char *ptag)  
{  
    ptag=(char *)malloc(40);  
    strncpy(ptag,"pointer tag", sizeof(char)*40);  
}

void process()  
{  
    char *tag;  
    gettag(tag);  
    free(tag);  
}
  1. tag는 init되지 않은 char pointer
  2. gettage의 Passing Param으로 전달됨
  3. ptag에 heap을 할당받아 그 시작 주소를 넣음
1
2
3
4
5
6
7
8
9
void gettag (char ptag){  
    ptag = 10;  
    return;  
}

void process(){  
    char tag;  
    gettag(tag);  
}
  • 다음과 같이 설정한 경우, process()::tag의 값이 제대로 설정되지 않는다!
1
2
3
4
5
6
7
8
9
10
11
12
void gettag (char **ptag)  
{  
    *ptag=(char *)malloc(40);  
    strncpy(*ptag,"pointer tag", sizeof(char)*40);  
}

void process()  
{  
    char *tag;  
    gettag(&tag);  
    free(tag);  
}
  • 다음과 같이 2중 포인터를 설정해야 tag값을 제대로 저장할 수 있음

ⓒ struct와 typedef 그리고 PACKED


Struct

  • struct 키워드는 구조체를 가진 변수를 만드는 키워드
1
2
3
4
5
6
7
8
struct customer{
	char name;
	int height;
	int weight;
}

customer kim[100];
customer *kim;
  • customer 구조체의 크기는? 1byte + 4byte + 4byte = 9byte?
    • 실제로는 내부에서 실제 자리 크기와는 상관 없이 4byte 단위로 alignment함
    • 12byte를 사용함
  • __packed 키워드를 사용하여 byte alignment 할 수 있음
    • 이를 이용해 9byte로 줄일 수 있음
    • Embedded System과 PC Host가 USB 통신을 할 때, PC는 int를 4byte로 인식하고, Embedded system에서는 2byte로 인식한다면, 서로 같은 데이터라도 막상 packet 형태로는 다르게 인식되는 문제 발생
    • 이런 문제를 해결하기 위해 Data의 byte alignment는 byte로 하기로 결정.
    • 물론 메모리 효율도 좋음
    • GNU에서는 __attribute__((packed))로 표현함

Typedef

  • struct키워드를 깔끔하게 쓰기 위해 typdef를 사용함
1
2
3
4
5
6
typedef unsigned char byte;
typedef unsigned char unit8;
typedef unsigned short word;
typedef unsigned long dword;

char a; byte a; unit8 a; // 모두 같음
  • 자신이 보기 편한 data형으로 이름을 바꿀 수 있다
1
2
3
4
5
6
7
8
typedef struct customer{
	char *name;
	int height;
	int weight;
} customer_type;

customer_type lee;
customer_type *dukgu;
  • customer_type을 데이터형으로 재정의함
  • 보통은 customer_type등과 같이 type의 의미를 덧붙여 헷갈림을 방지하여 명명함
  • 재귀적으로 자기가 자기 모양을 갖는 member를 갖게 하고 싶은 경우(linked list등 노드가 필요한 경우)
1
2
3
4
5
6
typedef struct heap_node{
	char filename [SIZ_FILE_NAME];
	unsigned int line_number;
	void *allocated;
	struct head_node *next;
} heap_node_type;
  • 여러가지 경우를 가지는 case를 구현할 때 사용함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef enum{
	START,
	WALK,
	RUN
} customer_activity_type;
customer_activity_type activity;

switch(activity){
	case START:
		~~~
		break;
	case WALK:
		~~~
		break;
	case RUN:
		~~~
		break;
}

ⓓ STACK, HEAP에 관한 소고


STACK

  • 자료구조 stack이 아닌 메모리 영역의 STACK에 대한 설명
  • tcc를 통해 compile 하게 되면, 함수 호출 시 어떤식으로 구현되는지,
    stack이 메모리에 어떻게 자리잡는지, option은 어디있는지, 실제로 stack이 어떤식으로 이용되는지는
    “함수의 구조와 함수가 불렸을 때 일어나는 일”에서 다룰 예정

Heap

  • Stack과 유사하나, 동적 할당에 사용함
  • 특히 Linked list, Tree 구조 등에 많이 사용함
    • 동적 할당 : 가변적인 양만큼의 data를 처리하기 위해 사용하는 할당 방법
    • 필요한 만큼만 꺼내 쓴다
    • alloc, free를 통해 메모리를 빌려오고, 반납한다
1
2
3
4
void HEAP(int n){
	int *p;
	p = (int *)malloc((sizeof(int))*n);
}
  • integer size를 n개만큼 연속적인 Memory 영역을 heap에서 가져온다.
  • *p를 통해 첫번째 원소에 접근 가능. 배열처럼 사용 가능
  • Fragmentation(조각화, 파편화) 문제 발생.
    • 메모리를 사용후 반납하지 않으면 메모리 누수 발생!
    • 특히, pointer를 local로 선언한 다음, free해주지 않으면, 그 함수가 끝나 return 되어버려서, 돌려줄 방법도 없어져버림

ⓔ Stack의 정체와 자세히 보기 - initialization까지


  • 임베디드 시스템에서는 Heap, Stack 모두 전역변수의 배열로 선언되어있음
  • 메모리 상에 일단 어떤 영역을 확보한 후, 이 영역에 대해 어떻게 처리할 것이냐에 의해 Stack인지 Heap인지를 구분함
  • Stack이나 Heap도 Bootup시, 내가 원하는대로 여러가지 종류로 선언하여 사용할 수 있음
    • .bss section ZI중 하나 memory
  • 보통 Stack은 높은 주소에서 낮은 주소로, Heap은 낮은 주소에서 높은 주소로 쌓아감
  • 자신의 시스템이 stack이나 heap이 어떻게 데이터를 쌓는지 알아두어야, Stack Back Tracking 이 가능
  • stack recipe_stack[200000]이라면, stack중 가장 늦게 쌓이게 되는 stack limit은 recipe_stack[0], 가장 먼저 쌓이는 곳은 recipe_stack[199999]가 될것임 stack
  • 스택 구현의 4가지 방법
    • ascending은 높은 주소로 자라는 방법, decending은 낮은 주소로 자라는 방법
    • 현재 stack pointer가 방금 push, pop한 데이터를 포함하면 Full, 아니라면 empty로 구분
    • Stack pointer가 데이터를 넣은 후 변하느냐 아니면 먼저 변하느냐에 따라 After, Before로 구분함
  • Stack은 Multiple register transfer addressing 명령어를 이용해 push, pop을 함
    • ST/LD : store, load
    • M : multiple
    • IB/IA/DB/DA : Increase Before/After, Decrease Before/After
    • FA/FD/EA/ED : Full Ascending/Decending, Empty Ascending/Decending
  • ARM state와 Thumb state에서의 stack의 명령어가 다름

ⓕ 함수가 불렸을 때 일어나는 일. - Stack 뒤지기 신공


  • 함수를 호출했을 때 일어나는 일
1
2
3
4
5
6
7
8
9
10
A()  { 	
	B(); 
	    
}

B()  {  C();  }

C()  {  D();  }

D()  {  ... }
  • 다음과 같은 함수가 있을 때, A() -> B() -> C() -> D() 순으로 호출함

fc

  • A() 입장에서는 ★ 위치로 복귀해야함
    • 이를 stack에 복귀할 위치를 저장해 두어 복귀함
  • 서브 루틴 호출 시 수행되는 일
    1. 전달 인자와 돌아갈 주소를 스택에 Push하는 일
    2. 함수 호출(즉, pc를 불려진 함수의 주소로 jump 시킴)
    3. 지역 변수에 대해 스택에 저장공간을 할당
    4. 호출된 함수를 수행함
    5. stack에서 할당된 지역변수 저장 공간의 해제
    6. 돌아갈 주소를 stack으로부터 꺼내와서 함수로부터의 복귀
    7. 전달인자에 의해 사용되었던 공간을 해제

ⓖ Stack 동작의 비밀과 실제 메모리 덤프


  • 예시를 통해 Stack 분석
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
N _ R0 FFFE R8 0  
Z Z R1 0 R9 0  
C C R2 5074 R10 0  
V _ R3 1E6C1A35 R11 0  
I _ R4 0 R12 0  
F _ R5 0 R13 **1F6E92C8**  
T T R6 FFFF R14 1E6C1A1D  
J _ R7 1F6E943C PC 1E6C19BC  
usr SPSR CPSR 60000030  
Q _  
A _ USR: FIQ:  
E _ R8 0 R8 0  
 R9 0 R9 C5400100  
0 _ R10 0 R10 1F62C9F0  
1 _ R11 0 R11 F00898F0  
2 _ R12 0 R12 DD  
3 _ R13 1F6E92C8 R13 1  
 R14 1E6C1A1D R14 F0008BFC  
 SPSR 10  
 SVC: IRQ:  
 R13 E000A880 R13 F008CD00  
 R14 F0000000 R14 1DB55BF8  
 SPSR 60000010 SPSR 60000010  
  
 UND: ABT:  
 R13 60000010 R13 00100000  
 R14 1DB55BF8 R14 1EFCE4BE  
 SPSR 60000010 SPSR 20000030
  • CPSR : User Mode, Thumb mode로 실행중. IRQ나 FIQ는 enable
  • PC는 1E6C19BC를 가리킴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
addr/line__|code_____|label____|mnemonic________________|comment________  
  |  
  |word b_funct(word arg, word param)  
  3958|{  
ST:1E6C19B8|B570  b_funct:  push  {r4-r6,r14}  
ST:1E6C19BA|B0B2  sub  sp,#0xC8  
  |  int loop;  
  3960|  word ret = 0;  
ST:1E6C19BC|2400  mov  r4,#0x0  // PC는 여기를 가리킴
  |  word array[100];  
  |  
  3963|  for (loop=0; loop < 100; loop++)  
ST:1E6C19BE|2200  mov  r2,#0x0  
ST:1E6C19C0|466E  cpy  r6,r13  
  |  {  
  3965|  if (loop%2)  
ST:1E6C19C2|07D3  lsl  r3,r2,#0x1F  ; r3,loop,#31  
ST:1E6C19C4|D002  beq  0x1E6C19CC  
  3966|  array[loop] = arg;
  • word b_funct(word, arg, word param)함수를 실행 중
    • Stack Pointer를 통해 어떤 함수가 얘를 호출했는지 확인 가능
    • 1F6E92C8 에서 호출
  • 이런식으로 Stack을 거꾸로 확인할 수 있다면, 소프트웨어의 동작 과정을 추적할 수 있음

ⓗ Stack Size는 어떻게 잡는가


  • 실무에서 사용하는 Stack 측정 방법
  • Stack의 값을 최대로 많이 사용 했을 때의 최대 값을 사용하고
    • 그 최대 크기를 전체 Stack크기의 3/4으로 잡아 System을 구성함.
  • ex) Stack의 시작 주소가 0x1000이고, 가장 많이 push해서 쌓았을 때 주소가 0x400이라면,
    • 0x2000-0x1400 = 0xC00이므로, x * 3/4 = 0xC00 이므로, 0xC00에 4/3을 곱하면 0x1000.
    • 0x1000~0x2000까지를 Stack으로 잡으면 된다
    • ⓘ 함수 포인터와 실행주소 변경


  • 함수의 이름은 Symbol. 물리적인 주소를 점유함
  • 실제 함수의 이름이 뜻하는 것은 실행 코드 영역에서의 함수의 시작 주소를 의미함
  • 함수 이름 자체를 어딘가의 포인터에 연결하여, 포인터가 가리키는 곳을 실행 시키면 함수를 실행시킬 수 있음

    1. 함수 포인터의 선언

  • 자료형 (* 함수포인터이름)(인자목록)
    • int (*function)(int a); vs int* function(int a);
    • 첫번째는 함수 포인터를 이용한 함수
    • 두번째는 function 이라는 함수가 integer 형 포인터를 return받는다. 전혀 다른 함수임

      2. 함수 포인터 Array

  • 사칙연산 함수를 넣는 예시
    • int (*functions[3])(int, int) = {plus, minus, multiply ,divide}
      • function[2](1,3) : multiply(1,3)을 호출하는 것과 같음
    • for(int i=0; i<4; i++) printf("%d\n", function[i](a,b));와 같이 반복문도 쓸 수 있다

      3. 함수 포인터의 응용 -> Device Drivers

  • 함수 포인터는 디바이스 드라이버들을 그때그때 다르게 쓰고 싶을 때 사용하면 좋음
    • 주변 Device들 중에서 같은 기능을 하지만, 여러가지 Vendor의 Device들을 한꺼번에 지원하고 싶을 때
  • 어떤 device가 read, write 기능을 한다고 가정했을 때, 아래와 같은 포인터 함수 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct {  
    const char  *name;  
    void (*read) (byte *buffer, int count);  
    void(*write) (byte *buffer, int count);  
} device_type;

device_type device =  
{  
    "It's me",  
    device_read,  
    device_write  
}

device_type *device_target;  
device_target = &device
  • 이렇게 해주면 사용자 입장에서는 (*device_target->read)(buffer, count)로 호출 가능
    • device_target = &device2로 선언하면 다른 드라이버로 쉽게 교체 가능

      4. 함수 포인터와 typedef

  • typedef int (function)(void)
    • function이라는 것이, int return값을 받으며, void 인자를 받는 함수
  • function *temp = hello;
    • hello()가 int return 값을 가지며, void 인자 값을 갖는 함수
    • temp()를 호출하면 hello()를 호출하는 것과 같음

      5. 함수 포인터의 완전 응용 -> 원하는 주소로 억지로 branch

  • 임베디드 시스템 개발 중, 특정 주소로 branch가 필요한 경우가 있음
1
2
3
void (*example) (void);  
example = (void (*)())0x7777;  
(*example)();
  • 위와 같이 억지로 pc를 0x7777로 만들어주는 효과
  • 혹은 (*(void(*)())0x7777)();와 같이 한줄로 구현 가능

ⓙ Linked List와 Queue


  • Queue
    • FIFO
    • IPC에 많이 사용되므로, 잘 알아둘 것
    • put, get을 사용함
    • 구현 방법이 다양함. array, linked list, 등
    • front, rear를 통해서 slide window 형식으로 valid한 data를 모아놓고, data를 넣을 때 rear쪽에, data를 뺄 때는 front쪽에서
    • overflow를 방지하기 위해 Ring buffer를 사용하기도 함
  • Linked List
    • 서로 다른 데이터 공간에 있어도 pointer를 통해 연결되는 자료 구조
    • Linked List를 이용한 Queue의 구현이 RTOS Kernel에 많이 사용됨
      • Free Queue를 사용
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.