자료구조, 알고리즘/x64 assembly

쌩 어셈블리로 두수 더하기

lok2h4rd 2021. 12. 28. 21:50

회장님의 권유로 해본 동아리발표에서 마지막에 짤막하게 extern를 통해 C언어 함수를 사용하여 코드를 작성하는 것과 syscall에만 의존하여 코드를 작성하는 것을 비교하기 위해 넣어봤던 코드입니다
사실 해당 코드도 다 설명하고 싶었는데 그때 당시 너무 바쁜 관계로 준비가 너무 부실해서 그땐 해당 코드 설명은 스킵했습니다.... 발표하고 너무...현타가 하..
인턴 + 교양팀플 + 학과 과제 및 수업 + 포너블팀 과제 + 포너블팀 발표 + 동아리발표... + 드림핵(?) 정신나간 일정에서 돌아와서 이제라도 정리해보려고 합니다

full code


코드만 단순히 보았을때 두개의 양수에 대한 합을 출력해주는 코드입니다 단순 비교를 위한 코드였기도 하고 구현에만 치중하다보니 코드의양 또한 너무 늘어난 것 같습니다 그래도 백준문제에 제출해보면 정상적으로 맞다고 찍히기 때문에 이대로 버리기는 아까워서 글이라도 남겨놓으려고 합니다

test

입력을 받게 되면 sys_read로 받다보니 그냥 아스키값으로 메모리(스택에)에 저장됩니다 이때 주의해야 할 점이 정수형태로 저장되는 것이 아니다보니 아스키 값을 정수로 변환시켜주고 이것을 또 더해주어서 올바른 값이 되도록해주어야 합니다
두 수의 길이 또한 정해져있지 않기 때문에 공백과 줄바꿈(0xa)를 통해 구분지어 계산해주어야합니다

input

main함수의 가장 처음에서는 사용할 스택을 확보한 뒤이를 0으로 초기화시켜주고 syscall을 통해 num1과 num2를 입력 받습니다
12345과 99999를 입력으로 주었다고 가정하였을 때 스택을 확인해보면

stack

정수가 아닌 문자열로 인식되어 저장되는 것을 확인할 수 있습니다

num1 loop

입력을 받은 뒤에 반복문을 돌면서 공백이 나올때까지 즉 num1을 .data영역에 미리 선언해두었던 num1이라는 배열에 byte단위로(한수 씩) 저장시킵니다 왜 여기서 0x30을 빼고 정수 형태로 저장하지 않은 이유는..... 그러게요 왜 그랬지..
그래도 이후에 연산을 진행할때 0x30을 빼주기 때문에 여기서는 넘어가도록 하겠습니다

위처럼 반복문을 돌다 공백을 마주치게 되면 num1_len으로 점프하게 됩니다 이후에 [rbp-0x8]을 1감소 시키는데 이는 첫번째 N번째 자리수의 인덱스를 알아내어 [rbp-0x18]에 저장하기 위함입니다
굳이 왜 이게 필요하냐면 가장 오른쪽의 수는 N 자리수이기 때문에 몇 자리의 수인지를 알 수 없기 때문에 길이를 알게되면 이후 연산에서 아스키코드를 정수로 올바르게 바꿔주기 위함입니다 (이후에 다루긴 합니다)
[rbp-0x8]을 1증가 시킨 이유는 공백은 수에 포함되지 않기 때문에 해당 인덱스(?)를 넘기기 위함입니다

num1 len and data

위의 과정을 모두 수행한 뒤 [rbp-0x18]과 num1의 수들을 확인해보면 위와 같이 저장된 것을 확인할 수 있습니다

num2_loop

다음에는 num2_loop이 수행됩니다 지금 생각해보면 num1데이터를 저장하는 배열의 주소와 num2데이터를 저장할 데이터의 주소를 스택에 저장하고 num2를 검사할 때는 스택을 8만 증가 시킨뒤 해당 주소를 사용하였다면 2개의 반복문을 사용하지 안해도 될 것 같은데..... 지금보니 참...오늘은 너무 힘들어서 나중에 시간내서 수정해봐야 될 것 같습니다

여기서는 이전 num1의 반복문과 동일하지만 줄바꿈(0xa)까지 반복문을 돌리면서 byte값을 num2배열에 저장합니다
rbp-0x10에는 num2의 N자리수의 인덱스가 저장되어 있습니다

num2 len and data

num2_loop 수행 후 num2의 N의 자리 인덱스와 데이터가 저장된 것을 확인할 수 있습니다

add_num(len, addr)

1번째 인자로는 인덱스 2번째 인덱스에서는 주소를 주소 add_num함수를 호출하고 그에 대한 반환 값을 [rbp-0x28]에 저장하게 됩니다

add_num

해당 함수에서는 첫번째 인자(rdi)를 1씩 감소시키고 해당 인덱스를 정수로 바꾼 뒤 이를 1, 10, 100, 1000 ... 단위로 곱하여서 이것들의 총 합의 정수 값을 반환 시킵니다

make_res and print

이후에 합을 인자로 make_res를 수행한뒤 res배열에 저장된 문자열을 출력하고 프로그램이 종료됩니다

지금 체력상 하나하나 보기에는 너무 몸이 힘들어서 과정만 보면 더해진 정수를 하나하나 문자로 변환하고 마지막에 줄바꿈(0xa)을 추가한 데이터를 res라는 전역 배열에 저장하고 해당 데이터들의 길이를 반환해주는 함수 입니다

global main


section .text
	add_num:
		push rbp
		mov rbp, rsp
		sub rsp, 0x20
		xor rax, rax
		mov [rbp-0x8], rax	;index
		mov [rbp-0x10], rax	; num 1 --> 10 --> 100 ... etc
		mov [rbp-0x18], rax	; result
		mov [rbp-0x20], rax	; array addr
		
		mov [rbp-0x8], rdi	
		mov [rbp-0x20], rsi	
		mov byte[rbp-0x10], 1
	add_loop:
		mov rax, [rbp-0x8]
		cmp rax, 0
		jl add_exit
		mov rcx, [rbp-0x20]
		mov al, byte[rcx + rax]
		sub rax, 0x30
		mov rbx, [rbp-0x10]
		mul rbx
		add [rbp-0x18], rax
		
		mov rax, [rbp-0x10]
		mov rbx, 0xa
		mul rbx
		mov [rbp-0x10], rax
		
		dec qword[rbp-0x8]
		jmp add_loop

	add_exit:
		mov rax, qword[rbp-0x18]
		add rsp, 0x20
		leave	
		ret
	
	make_res:
		push rbp
		mov rbp, rsp
		sub rsp, 0x20
		xor rax, rax 	
		mov [rbp-0x8], rax	; num1 + num2
		mov [rbp-0x10], rax	; index
		mov [rbp-0x18], rax	; return len(result + "\n")
		mov [rbp-0x20], rax


		mov [rbp-0x8], rdi
	res_loop:
		mov rax, [rbp-0x8]
		test rax, rax
		je make_string
		xor rdx, rdx
		mov rbx, 0xa
		div rbx
		
		mov [rbp-0x8], rax
		mov rax, [rbp-0x10]
		add dl, 0x30
		mov byte[res + rax], dl
		
	
		inc qword[rbp-0x10]
		jmp res_loop
	make_string:
		mov rax, [rbp-0x10]
		mov byte[res + rax], 0xa
		inc rax
		mov [rbp-0x18], rax
		xor rax, rax
		mov [rbp-0x8], rax
		dec qword[rbp-0x10]	
	mk_string_loop:
		mov rax, [rbp-0x10]
		mov rbx, [rbp-0x8]
		cmp rax, rbx
		jle res_func_exit	
	
		xor rcx, rcx
		xor rdx, rdx
		mov cl, byte[res + rax]
		mov dl, byte[res + rbx]
		
		mov byte[res + rax], dl
		mov byte[res + rbx], cl

		inc qword[rbp-0x8]
		dec qword[rbp-0x10]
		jmp mk_string_loop
	res_func_exit:
		mov rax, qword[rbp-0x18]
		leave
		ret	


	main:
		push rbp
		mov rbp, rsp
		sub rsp, 0x40
		xor rax, rax	
		mov [rbp-0x8], rax		; num1 index
		mov [rbp-0x10], rax		; num2 index || length
		mov [rbp-0x18], rax		; num1 length	
		mov [rbp-0x20], rax		; 
		mov [rbp-0x28], rax		; ======
		mov [rbp-0x30], rax
		mov [rbp-0x38], rax	
		mov [rbp-0x40], rax		; input

		mov rax, 0
		mov rdi, 0
		lea rsi, [rbp-0x40]
		mov rdx, 0x2c
		syscall				; input num1 num2

	num1_loop:				; int i = 0 ;
		mov rax, [rbp-0x8]		; while(1){
		mov al, byte[rbp-0x40 + rax]	;
		cmp al, 0x20			; 	if(input[i] == 0x20)
		je num1_len			;		 break;
						;
		xor rcx, rcx			;  	num1[i] = input[i];
		mov cl, al			;
		mov rax, [rbp-0x8]		;  i++;
		mov byte[num1 + rax], cl	; }

		inc qword[rbp-0x8]
		jmp num1_loop
	num1_len:				 
		mov rax, [rbp-0x8]		
		dec rax				
		mov [rbp-0x18], rax		; num1 length
		inc qword[rbp-0x8]		
	num2_loop:
		mov rax, [rbp-0x8]		; int j = 0; 	
		mov al, byte[rbp-0x40 + rax]	; while(1){
		cmp al, 0xa			;
		je num2_len			;	if(input[i] == 0xa) 
						;		break;
		xor rcx, rcx			;
		mov cl, al			; num2[j] = input[i];
		mov rax, [rbp-0x10]		; i++;
		mov byte[num2 + rax], cl	; j++;
						;
		inc qword[rbp-0x8]		;
		inc qword[rbp-0x10]		; }
		jmp num2_loop			
	num2_len:
		dec qword[rbp-0x10]		; num2 length
	
		mov rdi, [rbp-0x18]		; num1 length
		lea rsi, [num1]			; num1 address
		call add_num
		mov [rbp-0x28], rax		; result = num1
		mov rdi, [rbp-0x10]		; num2 length
		lea rsi, [num2]			; num2 address
		call add_num
		add [rbp-0x28], rax		; result = num2
			
		
		mov rdi, [rbp-0x28]		
		call make_res 			; make_res(result)
		mov rdx, rax			; length of result + \n
		mov rax, 1
		mov rdi, 1
		lea rsi, [res]			
		syscall				;print's out the result which it's result number in string

		xor rax, rax
		leave
		ret


section .data
res TIMES 21 db 0
num1 TIMES 21 db 0
num2 TIMEs 21 db 0


처음으로 printf나 scanf를 사용하지 않고 입력출력을 수행하는 코드를 짜봐서 더욱 스택이나 코드가 돌아가는 방식에 대해 이해가 된것 같습니다... 지금 다시 보면서도 쓸데없는 코드들이 보이긴한데 나중에 수정되면 다시 올리는걸로...
이제 다시 포너블하러..