push & 스택 푸쉬
리눅스 커널 함수를 어셈블리 코드로 열어보면 바로 push란 명령어가 눈에 보입니다. 그럼 정말 맞는지 샘플 코드를 볼까요? 아래 코드는 리눅스 커널 핵심 함수입니다. 각 함수에서 가장 먼저 실행되는 명령어가 push죠.
NSR:C0FF413C|__schedule:   push    {r4-r11,r14}
NSR:C0FF4140|              add     r11,r13,#0x20    ; r11,r13,#32
NSR:C0FF4144|              ldr     r3,0xC0FF4948
 
NSR:C017B0C4|handle_irq_event_percpu:  push    {r0-r2,r4-r11,r14}
NSR:C017B0C8|                          cpy     r3,r13           ; r3,sp
NSR:C017B0CC|                          ldr     r6,[r0,#0x4]
 
그럼 이 push란 명령어가 실행되면 어떤 동작을 할까요? 당연히 스택에 레지스터를 저장하죠. 이 때 스택 주소가 업데이트 됩니다. 그런데 이 동작은 ARM 프로세스의 함수 호출 규약과 연관돼 있습니다. 그래서 두 코드 “push {r4-r11,r14}”, “push {r0-r2,r4-r11,r14}”가 실행되면 스택 메모리에 정확히 어떤 값들이 업데이트되는지 확인이 필요합니다.
 
자 이제 시작하기 전에 한 가지 조건을 말씀드릴께요. 여기서 각각 레지스터가 담고 있는 값들은 레지스터 번호와 같다고 가정할께요. 실제 코어 덤프에선 이런 패턴의 레지스터를 볼 수는 없는데요. 좀 더 이해를 돕기 위해서 이렇게 레지스터 번호와 레지스터가 담고 있는 값을 통일 시켰습니다.
r0: 0, r1: 1, r2: 2, r3: 3, r4: 4, r5: 5, r6: 6, … ,r14: start_kernel, 스택 주소: 0xD000D000
 
push {r4-r11,r14}
아래 함수가 실행되기 전에 스택 주소는 0xD000D000입니다.
NSR:C0FF413C|__schedule:   push    {r4-r11,r14}
 
"push {r4-r11,r14}" 명령어가 실행되기 전에 0xD000D000 스택주소 근처 메모리 들은 아래 값을 갖고 있습니다.
메모리주소       값
1 NSD:D000CFD8|0x0
2 NSD:D000CFDC|0x0   
3 NSD:D000CFE0|0x0 
4 NSD:D000CFE4|0x0    
5 NSD:D000CFE8|0x0  
6 NSD:D000CFEC|0x0   
7 NSD:D000CFF0|0x0     
8 NSD:D000CFF4|0x0     
9 NSD:D000CFF8|0x0      
10 NSD:D000CFFC|0x0
11 NSD:D000D000|0x0  // <<-- 스택주소
 
"push {r4-r11,r14}" 명령어가 실행되면 2가지 동작을 한번에 수행합니다. 
1. 스택 주소가 업데이트 됩니다. 스택에 푸쉬되는 레지스터 갯수만큼이죠. {r4-r11,r14} 명령어의 의미는 {r4, r5, r6, r7, r8, r9, r10, r11, r14} 인데요. 레지스터 갯수가 9개이고 32 비트 아키텍처이므로 4 x 9 = 36, 즉 36 바이트만큼 스택 공간을 확보합니다.
0xD000D000 - 0x24(36: 10진수) = 0xD000CFDC, // 10진수 36은 16진수로는 0x24 값이죠.
 
0x24 바이트 만큼 스택 공간 확보 후 새로운 스택 주소는  0xD000CFDC 가 됐습니다.
 
2. 레지스터 스택 푸쉬 
이전 동작에서 0x24 크기만큼 스택 공간을 확보했습니다. 이제는 레지스터들을 스택에 푸쉬할 차례입니다. {r4, r5, r6, r7, r8, r9, r10, r11, r14} 9개 레지스터들을 스택에 저장합니다.
메모리 주소     값
12 NSD:D000CFD8|0x0
13 NSD:D000CFDC|0x4   // <<-- 스택주소 
14 NSD:D000CFE0|0x5 
15 NSD:D000CFE4|0x6    
16 NSD:D000CFE8|0x7   
17 NSD:D000CFEC|0x8   
18 NSD:D000CFF0|0x9     
19 NSD:D000CFF4|0x10     
20 NSD:D000CFF8|0x11      
21 NSD:D000CFFC|0xC19008D4 \\vmlinux\init/main\start_kernel
22 NSD:D000D000|0x0
 
21번째줄 덤프에서 r14을 저장하고, 0xD000CFDC 부터 0xD000CFF8 메모리 주소에 레지스터를 저장합니다. 0xD000CFDC 메모리에는 r4,  0xD000CFE0 메모리에는 r5가 푸시된 거죠. 쭉 이런 방식으로 0xD000CFF8 메모리에는 r11을 저장합니다. 메모리 주소에 어떤 순서로 레지스터를 푸시하는지잘 기억하세요.
 
이번에는 handle_irq_event_percpu 함수 내 “push {r0-r2,r4-r11,r14}” 명령어가 수행되면 어떤 동작을 하는지 알아볼까요?
 
 
push {r0-r2,r4-r11,r14}
역시 이번에도 각각 레지스터가 담고 있는 값들은 레지스터 번호와 같다고 가정할게요. 
r0: 0, r1: 1, r2: 2, r3: 3, r4: 4, r5: 5, r6: 6, … ,r14: start_kernel, 스택 주소: 0xD000D000 
 
아래 함수가 실행되기 전에 스택 주소는 0xD000D000입니다.
NSR:C017B0C4|handle_irq_event_percpu:  push    {r0-r2,r4-r11,r14}
 
push 명령어가 실행하기 전 0xD000D000 스택주소 근처 메모리 들은 아래와 값을 갖고 있습니다.
메모리주소           값
1  NSD:D000CFD0|0x0
2  NSD:D000CFD4|0x0
3  NSD:D000CFD8|0x0
4  NSD:D000CFDC|0x0
5  NSD:D000CFE0|0x0
6  NSD:D000CFE4|0x0
7  NSD:D000CFE8|0x0
8  NSD:D000CFEC|0x0
9  NSD:D000CFF0|0x0
10 NSD:D000CFF4|0x0
11 NSD:D000CFF8|0x0
12 NSD:D000CFFC|0x0
13 NSD:D000D000|0x0
14 NSD:D000CFEC|0x0   
15 NSD:D000CFF0|0x0     
16 NSD:D000CFF4|0x0     
17 NSD:D000CFF8|0x0      
18 NSD:D000CFFC|0x0
19 NSD:D000D000(스택주소)|0x0
 
"push {r0-r2,r4-r11,r14}" 명령어가 실행되면 이번에도 다음과 같이 2가지 동작을 한번에 수행합니다. 
 
1. 스택 주소가 업데이트 됩니다. 스택에 푸쉬되는 레지스터 갯수만큼이죠. 
{r4-r11,r14} 명령어의 의미는 {r0, r1, r2, r4, r5, r6, r7, r8, r9, r10, r11, r14} 인데요. 
레지스터 갯수가 12개이고 32 비트 아키텍처이므로 4 x 12 = 48, 즉 48 바이트만큼 스택 공간을 확보합니다.
0xD000D000 - 0x30(48: 10진수) = 0xD000CFDC, // 10진수 48은 16진수로는 0x24 값이죠.
 
0x30 바이트 만큼 스택 공간 확보 후 새로운 스택 주소는   0xD000CFDC 가 됐습니다.
 
 
2. 레지스터 스택 푸쉬 
0x30 크기만큼 스택 공간을 확보했습니다. 이제는 레지스터들을 스택에 푸쉬할 차례입니다. 
{r0, r1, r2, r4, r5, r6, r7, r8, r9, r10, r11, r14} 12 개 레지스터들을 스택에 저장합니다.
메모리주소     값
NSD:D000CFD0(새로운 스택)|0x0   
NSD:D000CFD4|0x1        
NSD:D000CFD8|0x2       
NSD:D000CFDC|0x4      
NSD:D000CFE0|0x5        
NSD:D000CFE4|0x6      
NSD:D000CFE8|0x7   
NSD:D000CFEC|0x8      
NSD:D000CFF0|0x9     
NSD:D000CFF4|0x10     
NSD:D000CFF8|0x11      
NSD:D000CFFC|0xC19008D4 \\vmlinux\init/main\start_kernel
NSD:D000D000|0x0
 
push란 어셈블리 명령어에 대해서 배웠습니다. 실제 메모리에 push 명령어가 실행되면 어떤 값들이 저장되는지 알아봤구요. 다음에는 ldr이란 명령어를 살펴볼께요.

+ Recent posts