지옥방 스터디 포너블
실습환경 : Ubuntu 18.04.4, Nasm
어셈블리어 :기계어와 1 대 1로 대응되는 명령어 체계를 가진 컴퓨터 프로그래밍 언어
어셈블리어 문법은 두가지가 있다 => 윈도우 사용 : Intel 문법 / 리눅스 사용 : AT&T 문법
opcode 명령어 또는 연산자의 의미를 갖는다.
operand 데이터 또는 피연산자의 의미를 갖는다
opcode가 '더하라'이고 operand가 3,4라면 3과 4라는 더하라는 명령이라고 이해할 수 있다.
메모리
메모리의 기본 단위는 1 byte이다. 메모리에서 각 byte는 메모리 주소라는 고유 숫자가 붙어있다.
메모리 단위로는 word = 2 바이트, double word = 4 바이트, quad word = 8 바이트, paragraph = 16 바이트 . 메모리의 모든 데이터는 숫자로 되어 있다.
어멋!? 레지스터가 뭐야~?
레지스터는 cpu안에 존재하면서 연산을 수행하는 녀석이다. 컴퓨터 안에서 제일 빠른 연산 속도를 가진 녀석이기 때문에 이 친구가 도맡아한다. 근데 용량이 작아서 레지스터에 프로그램을 받아서 연산을 돌릴 수는 없다. 그래서!!?? 프로그램은 ram에서 맡고 ram이 프로그램을 돌릴 때 cput의 레지스터와 교류를 하며 연산을 수행한다.
범용 레지스터 우리가 사용 가능한 레지스터이며, 산술/논리 연산, 오퍼랜드(피연산자)를 저장, 포인터의 역할로 사용한다
EAX(AX, AH, AL) - 누적연산기, 곱셈과 나눗셈 연산에서 자동으로 사용. A는 Accumulate의 A 같다.
EBX(BX, BH, BL) - 베이스 레지스터, 특정 주소를 지정한다. B는 Base의 B 같다.
ECX(CX, CH, CL) - 수를 셈, 자동으로 루프 카운터 된다. (반복적인 명령 수행시) C는 Count의 C 같다.
EDX(DX,DHT, DL) - 데이터 레지스터, 입출력 연산에서 반드시 간접 주소 지정에 사용. Data의 D.
EBP(BP) - 베이스 포인터, 스택의 데이터에 접근하기 위해 사용. Base Pointer의 BP
ESP(SP) - 스택 포인터, 현재까지 사용된 스택의 위치를 저장, 스택 최상부의 오프셋을 가리킴. Stack Pointer의 SP
ESI(SI) - 읽기 인덱스, 문자열 전송이나 비교에서 사용되는데 주로 소스 문자열의 오프셋을 가리킴. Source Index의 SI
EDI(DI) - 쓰기 인덱스 Destination의 D
EIP(Instruction Pointer) - 명령어 포인터 레지스터, 실행할 다음 명령어의 주소를 포함.
EFLAGS - CPU의 동작을 제어하거나 CPU 연산의 결과를 반영
세그먼트 레지스터
CS(Code Segment) - 코드를 저장하는 메모리 블록
DS(Data Segment) - 데이터를 저장하는 메모리 블록
EX(Extra Segment) - 비디오와 관련된 것을 위해 사용됨
SS(Stack Segment) - 루틴으로부터 리턴 어드레스를 저장하기 위해 프로세서에 의해 사용되는 레지스터
인덱스 레지스터
SI(Source Index) - 문자열/배열의 소스를 지정하기 위해 사용됨.
DI(Destination Index) - 문자열/배열의 목적지를 지정하기 위해 사용됨.
IP(Instruction Pointer) - 다음 명령의 주소를 지정함! 그래서 그 값을 직접적으로 변경 불가능.
스택 레지스터
BP(Base Pointer) - 스택 오퍼레이션을 위해 SP와 연결되어 사용됨
SP(Stack Pointer)
특별한 목적을 위한 레지스터.
IP(Instruction Pointer) - 실행된 명령의 offset을 가지고 있다.
Flag - 분기(branching)를 위해 사용. 플래그 레지스터는 크기가 1 바이트이다.
MOV 연산자
형식은 MOV 목적지, 값 이다.
MOV AX, 56h
AX를 56h와 같게 한다.
MOV AX,BX
AX에 BX의 값을 복사 붙여넣기 한다.
XCHG 연산자
단순히 두 레지스터를 서로 바꾸는 연산자. exchange에서 나온 XCHG~
형식은 XCHG 레지스터1, 레지스터2 이다.
위의 코드에서는 DX에 56h라는 값을, AX에 3Fh라는 값을 복사하고, XCHG 연산자를 사용하여 DX와 AX의 값을 서로 바꿨다.
즉 위의 예에서는 AX는 56h, DX는 3Fh의 값을 갖고있다.
**주의** 8비트 (h/1) 레지스터와 16비트 (x)와 교환하지 않도록 해야한다. 이를 어기는 코드는 무.효.
INC와 DEC
INC는 increase, DEC는 decrease에서 나온 것이당.
DX의 값은 56h로 설정되었고 그 밑에서 INC 연산자를 사용해 DX의 값을 증가시켰다. DX는 51h가 되었다..!
DX의 값은 50h로 설정되었고 그 밑에 DEC 연산자를 사용해 DX의 값을 감소시켰다. DX는 4Fh가 되었다.
스택 조작 - POP PUSH
*형식 : POP 레지스터 PUSH 레지스터 *
나중에 사용하기 위해 스택에 있는 A의 값을 일시적으로 저장하고자 한다면 PUSH AX를 사용한다.
그리고 원래의 값을 회복하고자 한다면 POP AX을 사용하면 된당.
스택은 단지 16비트 레지스터만 받아들인다는 것 주목!
위의 코드를 실행했을때 AX, BX의 값은 몇일까~?
1.AX의 값을 51h로 설정, BX의 값을 4Fh로 설정
2.XCHG 연산자를 이용하여 둘의 값을 바꾼다.
3. PUSH AX로 현재 AX의 값을 일시적으로 저장한다. 그리고 MOV 연산자 AX의 값이 34h로 설정.
4.POP BX 로 BX의 원래 값을 회복. 즉 BX = 4Fh
5.PUSH BX로 BX의 값을 일시적으로 저장. 지금 BX = 4Fh
6. POP AX를 하면 AX = 34h 이전의 값을 회복한다. 즉 AX = 4Fh!!
그럼 BX는 4Fh, AX는 4Fh이다.
** 수학 연산자 조작**
ADD
형식은 ADD 레지스터1, 레지스터2 또는 ADD 레지스터, 값
ADD AX, BX => AX에 BX를 더하고, 그 값을 AX에 저장한다.
SUB
형식은 SUB 레지스터1, 레지스터2 또는 SUB 레지스터, 값
그냥 딱 ADD랑 반대
MUL
형식 : MUL 값(or 레지스터)
하나의 operand만 필요. 주어진 레지스터를 AX 또는 AH로 곱하기를 원한다고 추정하기 때문
DIV
형식 : MUL과 같다!
원리도 같다!
Bit 연산자! - 두 값을 bit 단위로 비교를 하는 것.
AND
-형식 : AND 레지스터1, 레지스터2 또는 AND 레지스터, 값
-두 어포랜드가 1일 경우만 1을 리턴,
-ex)
5h => 101b
6h => 110b
100b <= AX =4h
-둘다 1일때만!!!!
OR
-형식 : OR 레지스터1, 레지스터2 또는 OR 레지스터, 값
-하나라도 1이면 1 리턴
AX = 5h =>101b
BX = 6h =>110b
111b <= AX = 7h
XOR
-형식 : XOR 레지스터1, 레지스터2 또는 XOR 레지스터, 값
-XOR는 오퍼랜드가 만약 어느 하나가 1이면 1을 리턴하고, 둘다 1이면 0을 리턴.
AX = 5h => 101b
BX = 6h => 110b
011b <= 3h
NOT
-형식 : NOT 레지스터 또는 NOT 값
-이건 operand 하나 주어짐. 주어진 operand의 비트값을 반전시켜놓는다. 101b면 NOT 101b => 010b
** 여기서부터 순서가 이상해질 예정 **
section .data 에서 data란!?: 프로그램에서 사용되는 문자열같은 데이터를 저장할 수 있는 공간을 의미
section .text 에서 text란!?: 위에서 아래로 차례대로 내려가는 소스코드 그 자체를 의미
_start : main함수보다 먼저 실행되는 함수!!
<레지스터의 용도와 시스템 콜 이해하기>
stack : 함수에 대한 정보들 포함, 함수 내의 지역변수도 포함!
- ex) int sum(a,b) { } 에서 a,b가 지역변수
Heap : 동적으로 할당된 데이터들이 정의되는 곳 malloc()사용
BSS (uninitialized) : 프로그램에서 사용될 변수들이 실제로 위치하게 되는 영역 -> 초기화 x ex) input 1 resd 1
Data(initialized) : 초기화가 된 변수들이 위치하는 영역 ex) msg db "늦어서 죄송합니당,,",20,0
Text : 우리가 작성한 소스코드가 들어가는 영역
아까 만들었던 Helloworld 출력 프로그램에서 section .data에서 작성된 msg db "Hello World"는 위의 그림에서 Data 영역에 "Hello World"라는 문자열을 저장하게 된다.
데이터 지시어
데이터 지시어는 데이터 세그먼트에서 메모리상의 공간을 정의하는 데 사용된다. NASM에서는 메모리 공간을 정의하는 2가지 방법을 제공하는데, 매모리 공간만을 정의하는 resx 계열 지시어고, 다른 하나는 메모리 공간과 초기 값을 지정하는 dx 계열의 지시어이다. resx와 dx의 x에는 다음과 같은 문자가 들어갈 수 있다.
문자 | dx | resx | 단위(Unit) | another name |
b | db | resb | 1 byte |
바이트 |
w | dw | resw | 2 byte | 워드 |
d | dd | resd | 4 byte | 더블 워드(dword) |
q | dq | resq | 8 byte | 쿼드워드(qword) |
t | dt | rest | 10 byte | 텐 바이트(tbyte) |
아까 만들었던 Hello World를 출력하는 프로그램에서 작성했던 Section .data의 " msg db 'Hello World!', 10,0 "에서의 db가 바로 위 표의 데이터 지시어 db다.
<에코 프로그램 만들기>
xor rax, rax | rax와 rax에게 비트연산자 XOR을 사용했다. XOR은 서로 비트가 다른 값이여야 1을 뿜뿜하는데 같은 값끼리는 비트가 다를 수 없기 때문에 무조건 0이 나온다. |
mov rbx, rax | rbx에 rax의 값을 할당한다. 0! |
mov rcx, rax | rcx에 rax의 값을 할당한다. 0! |
mov rdx, rax | rdx에 rax의 값을 할당한다. 0! |
sub rsp, 64 | rsp의 값에서 64를 뺀다. 64 바이트 크기의 데이터 공간을 마련해줌으로써 입력받은 문자열이 저장될 공간을 형성해준다. |
mov rdi, 0 |
파일 디스크럽터는 무언가를 읽을때는 0, 쓰는 경우에는 1 여긴 읽는 경우이니 0! |
mov rsi, rsp | rsi에 rsp의 값을 할당한다. rsi는 입력받은 문자열을 가리킨다. rsp는 문자열을 저장할 공간의 첫번째 주소를 가리키기에 이 명령이 실행되면 주어진 공간에 문자열이 저장되게 된다. |
mov rdx, 63 | rdx에 63의 값을 할당한다. rdx는 입력받을 데이터의 크기가 저장된다. |
syscall | 띠드콜 |
mov rax, 1 | rax에 1을 할당시킨다. rax가 1일때 syscall은 sys_write 역할을 한다. 즉 그냥 출력ㅎㅎ |
mov rdi, 1 | 파일 디스크럽터인 rdi에 1이 할당되었으니 무언가를 쓰는 경우다! |
mov rsi, rsp | rsi에 rsp를 |
mov rdx, 63 | rdx에 63을 할당한다. rdx에는 출력할 데이터의 크기가 주어진다. |
syscall | 띠드콜 |
mov rax, 60 |
rax에 60이!? 이것은 sys_exit을 뜻한다,,즉 종료닷 |
syscall | 띠드콜 |
LEA : A의 값을 B의 값으로 연산을 포함하여 복사합니다. LEA EAX, [EAX+1000]은 EAX에 1000을 더한 값을 EAX에 저장한다.
JMP : 특정한 위치로 건너 뛰어 코드를 실행합니다. JMP A라고 하면 A의 위치로 뛰어서 코드가 실행됩니다. JA, JB, JE 등의 명령어는 두 인자를 받아서 비교한 뒤에 결과에 따라서 다른 방향으로 점프할 수 있습니당.
CALL : 함수를 호출했다가 다시 원래 위치로 돌아올 때 사용합니다. JMP와 다른 점은 실행한 뒤에 끝나게 되면 RET에 저장하고 다시 원래 상태로 돌아온다는 점이다.
NOP : 아무것도 하기싫다. 아무것도 하고 있지 않은데 더더욱 아무것도 하기 싫다. 1 바이트의 빈 공간 차지
RET : 현재 함수가 끝난 뒤에 돌아갈 주소를 지정하기 위해 사용한다.
LEAVE : 현재까지의 메모리 스택을 비우고 EBP를 자신을 호출한 메모리 주소로 채웁니다. 실행 중인 함수를 종료하기 위해 정리하는 작업에 사용된다!
<반복문>
mov rax, 1 | rax가 1이에요. 출력하겠다 이 말이죠!? |
mov rdi, 1 | rdi가 1이에요! 쓰겠다 이거죠!? |
mov rsi, msg | rsi가 msg에요. 출력할 문자열이 'A'라는거죠!? |
mov rdx, 1 | rdx에 1을 할당해요 |
mov r10, 1 |
r10에 1을 할당해요 |
cmp r10, 100 | r10이랑 100이랑 비교를 해요 |
je done | 같다면!? 그만둬요! |
syscall | 띠드콜~ |
mov rax, 1 | rax에 1을 저장한다잉~ 이건 출력하겠다 이거다잉~ |
inc r10 | r10의 값을 +1한다잉~ |
jmp again | again으로 다시 가버려 |
mov rax, 60 | sys_exit 프로그램 종료~ |
mov rdi, 0 | 오류 없음! |
*어느 문서에서 봤는지는 기억이 안나지만.. 함수를 만들고 인자를 줄때는 정해진 인자 순서의 역순으로 스택에 push한다.
gdb를 이용하여 _start에서 브레이크를 걸어놨다.
레지스터들의 상황을 볼 수 있다. 하 졸리다.
RSP 특정 메모리를 가리키고 있다.
RIP 어떤 명령을 실행할지 가리키고 있다.
현재 _start에서 브레이크가 걸려있는걸 알 수 있다. 그 오른쪽에는 어셈블리어로 실행되는 명령어가 적혀있다.
터미널에 ni를 한 번 치면 하나의 단계를 넘어간다. 어.. 그니깐! 다음 명령어를 실행하려면 ni!
스택의 상황도 볼 수 있어요 엄청나죠?? 봐도 뭔지는 몰라요오 ㅎㅎㅎㅎㅎ
어셈블리 함수의 정의와 선언
== 어셈블리 ==
TEST PROC NEAR32
push ebp
mov ebp,esp
함수 내용부~
mov esp, ebp
pop ebp
ret
TEST ENDP
== C언어 ==
void TEST(void)
{
함수 내용부~~
}
위의 두 코드는 같은 기능을 한다. TEST라는 이름의 함수를 만들어준다.
메인은 어셈블리 함수이니 자세히 살펴보면
push ebp
mov ebp,esp 는 함수의 Entry Code라 하고 함수 시작부 주소의 설정과 호출되기 전 함수의 시작부 등을 설정해주는 기능을 한다. 꼭! 있어야하는 코드. Entry Code가 있으면 ? Exit Code도 있다잉
mov esp, ebp
pop ebp
ret Entry Code에서 변경 되었던 EBP를 다시 호출되기 전 값으로 복구 시키고 리턴 어드레스로 돌아가게 하는 역할을 수행
strlen, strcat, strcat 함수를 어셈블리어로 구현한다. 구현해보며 알게되는 사실은 여기다가 게시한다.
1. strcpy
strcpy(char* dest, const char* src, size_t_count) 의 형식으로 사용된다. 복사된 값이 저장될 문자열, 복사될 문자열, 복사할 문자의 개수가 주어진다. 흐음 이걸 어떻게 할까. 일단!? 문자열을 받아서,,그걸 저장을 해야겠찌,..? 그리고 데스티네이션 문자열에 그걸 대입해야겠찌,,?
size_t_count라는 이름으로 복사 붙여넣기할 크기를 알려주겠지..?
1)그럼! 문자열을 입력 받아보자. 도대체 문자열은 어떻게 입력받는걸까..?
친한 친구 한명에게 물어보고자 한다. 하씨 안나온다.
친구가 많아서 다행이다.
어,,,? 아닛???? 저것이 보이는가? 내 눈이 잘못됐나? 어..?
눈 좀 비비고
엇,,? 이것은? strlen!!????
2)strlen
문자열을 입력받고 문자의 갯수를 출력하는 함수다
주제를 바꾸고 저 글을 읽어보니 nasm 기준이 아니여서 모르는게 막 나온다. 큰일났다
하 큰일났다. 그래도 그나마 strlen이 만만하다. 해보자
예전에 구글링을 해봤을때 알게된 구현방식은
문자 하나하나를 null문자와 비교하며 반복문을 돌리는 것이였다.
해보자..! 프로그램의 초반은 위에서 만들었던 에코 프로그램을 그대로 썻다.
rax,rbx,rcx,rdx를 모두 0으로 초기화 해준다.
xor rax,rax는 rax를 0으로 초기화 시키는 것을 의미한다. 왜냐면!?
xor은 비트단위로 비교를 실행하는데, 1 + 0 =>1 이고 나머지는 다 0이다. 즉 같은 값끼리 비교하면 1 + 1 => 0, 0 + 0 =>0이므로
모두 0!
이제 한 문자씩 널 문자와 비교해야한다. 어!!?? 비교? 비교를 어떻게 하지?
cmp
비교 명령어다. 형식은 cmp opnd1, opnd2
cmp 명령은 내부적으로 opnd1과 opnd2를 빼는 과정을 거친다고 한다. 즉 플래그 레지스터의 값 변경이 발생한다고 한다!!!
이거 어뜩해!? 아 우린 문자열의 길이만 출력하면 되니 상관이 없다ㅎㅎ
cmp 로 비교를 해서 널 문자가 아니라면 rcx ( 반복문에서 갯수 세는 용도 )에 1씩 더하고 다음 문자를 똑같이 비교하게 하면 되겠다.
만약 널문자라면 반복문을 중단하고 그때까지의 rcx를 출력하면 되겠찌..?요?
jz 반복문을 빠져나오려면
아까 거기다. rsp에서 64를 빼줌으로써 입력되는 문자열을 저장할 공간을 만들어준다.
rax가 0이기 때문에 syscall을 실행하면 sys_read가 실행이 되는데 그럴려면
rdi에 입력받겠다는 숫자 0. rsi에 할당된 공간의 첫주소값. rdx에 입력받을 크기를 할당해줘야한다.
syscall을 하면 문자열을 입력받는다.
그럼 받은 다음 비교를 시작해봅시다.
rsi와 byte 0를 비교하고 있다. byte 0가 널문자라는 소문을 들었기 때문이다. rsi가 byte 0라면, 즉 널문자라면 _null_e_da (널이다!) 로 이동하며 반복문은 끝나게 된다. 그런데 여기서 조금 살짝 궁금한게,, 위에서 rsi에 주소값을 할당해놓지 않았나..? 근데 그 주소값으로 비교를 해도 되나 싶다. 모르겠다 일단 넘어가자
비교를 했을때 널문자가 아니라면 rcx (널문자 이전의 문자 개수)에 inc를 사용해 1을 더해준다
현재의 rsi는 비교를 했으니 다음 rsi를 비교하기 위해 rsi도 inc로 1 더해준다.
그리고 다시? 다음 문자를 비교하게 하기 위해
쩜프!!!!!!!!!!!!!!!! _strlen으로 다시 복귀
널문자를 만났을때 오게되는 그 곳,,
rcx를 출력하기 위해 rax도 1로 바꾸고 준비중
이대로 간다..
채널 고정
프로그램이 끝나지 않는다..왜라고 생각하는가..?
나는 잘 모르겠다.
그 시행착오 제가 겪어보겠습니다.
프로그램이 종료되지 않는 버그를 고치기 전에 아까 거슬렸던 rsi와 byte 0를 비교하는 부분을 보자.
내 짧은 지식으로 생각을 해봤는데,, 아무리 생각해봐도 rsi는 주소값이다. 주소값을 널문자와 비교하는게 어떤 의미가 있을까
없어!!!!!! 그 주소값에 할당된 값이랑 비교하지 않는 이상 의미가 없을것 같다는 결론이 나온다.
c언어에서는 주소값에 * (포인터)를 붙이면 그 주소에 저장된 값을 사용할 수 있지 않은가?
그게 어셈블리어에서는 뭘까!? 수소문을 시작해봤다.
자기 일처럼 정말 팔 벗고 나서주는 좋은 사람들이 많다. 이 세상은 아직 좋은 세상이라고 생각된다.
[]
그 안에 있는 값을 주소로 인식하여, 그 주소가 가리키는 곳을 찾아가라는 의미
와 그럼 아까의 코드를 수정해보자
이대로 가보자..!!!
뭘까 이 친구는.
차라리 영어로 말해줘 친구야ㅎㅎㅎㅎ
느낌이 온다.. segmentation error 라면,,
나를 놀리는걸까?
분할된 부분중 어느 하나가 잘못되었다는걸까..?
아까 맘에 걸렸던 부분이 하나 더 있다. 그걸 해결해보자
'널이다' 부분이다.
이제 다 정리하고 출력하는 부분인데, rsi가 출력 버퍼?니깐 거기다가 그동안 더해온 rcx 값을 복사해 출력하려는 의도였다.
근데 아까도 봤다싶이 rsi는 주소값이였다. 그럼 대괄호를 씌워볼까..?
요즘 욕을 참 많이 하는거같다.. 집 밖에는 벚꽃도 많이 피었으니 그걸 보며 마음의 안정을 되찾는게 중요할거같다.
이유가 뭘까
후우,,다시 시작해보자
아자아자 화이팅!
segment는 소스 코드에 영역별로 메모리를 정의하고 싶을 때 사용하는 어셈블러 지시어, segment 다음에 영역을 넘겨서 해당 영역데 메모리를 정의할 수 있도록 한다.
s db 'I love sak!',10,0
s 는 문자열 이름
db는 byte 형식의 데이터를 의미. 즉 s는 byte 배열로 정의된 레이블이다.
문자열 뒤에 콤마(,) 이후 10과 0 이라는 숫자가 나왔는데, 여기서 10은 개행 문자의 ASCII 코드 값이고, 0은 널문자(\0) 로 문자열을 끝내기 위해 0을 마지막에 넣었다.
위에서 segment에 대해서 잠깐 알아봤다. segment .text는 해당 지시어 다음에 등장하는 모든 소스가 코드 세그먼트에 대한 것이라고 어셈블러에게 전달하는 어셈블러 지시어다.
global main _main 레이블이 전역에 선언된 레이블임을 의미하는 어셈블러 지시어다. 기본적으로 어셈블리 언어의 레이블들은 모두 내부 선언되어 있기 때문에 global을 붙여주지 않으면 다른 파일에서 이 레이블에 접근하는 것이 불가능, 즉 global은 다른 파일에서 레이블에 접근할 수 있도록 만들어준당. 발이 넓은 친구 ㅎㅎ
_main _min 프로시저를 정의한다. 프로스저의 정의는 어셈블리에서 레이블의 정의와 같다! 아마도 procedure..?
문자열을 입력받아서 하던 아까와 달리 .bss 세그먼트에다가 s라는 이름의 "I love Sak!"+ 개행문자+ 널문자를 선언해놓았다.
이 문자열의 길이를 반환하는 strlen 프로그램을 만들어보고자 한다.
_start: |
하 시작이다 |
xor rax,rax | 위에서도 사용했던 초기화 방법이다. |
mov rbx,rax | |
mov rcx,rax | |
mov rdx,rax | |
mov esi, s | 주소값 esi 에 문자열 s를 대입해봤다, 여기서 문제가 있을까..? |
_strlen: | 본격적으로 문자열의 길이를 세는 구간이다. |
cmp [esi], byte 0 | byte 0 는 널문자를 의미, 주소값 esi에 있는 |
JE _stop | 만약 널문자가 발견된다면 _stop으로 이동 |
inc rbx | 위에서 널문자가 발견되지 않았다면 문자열 길이를 카운트하는 rbx에 1을 더해준다 |
inc esi | 이번 주소값에 있는 문자는 비교가 끝났으니 다음 비교를 위해 esi도 1을 더해준다. |
jmp _strlen | 다시 비교하기 위해 다시 위로 뿌슝 |
_stop: | 널문자가 발견되었을때 오는 곳이다. 출력을 준비한다. |
mov rax,1 | sys_write를 준비중 |
mov rdi,1 | |
mov rdx,3 | |
mov rsi, rbx |
rsi 주소값에 아까 카운트한 값인 rbx를 대입해보았다. 여기도 뭔가 이상하다,, |
syscall | 간닷 |
익숙한 친구다. ㅎㅎ
아까 찝찝했던 부분들을 고쳐보자
놀랍게도 방금 전의 에러와 다른 친구다 ㅎㅎㅎㅎ다른걸 고쳐보장
위 표에서 문제가 있을까?라고 생각했던 부분이다.
내 생각은 이랬다. esi는 주소값인데 거기다가 문자열을 할당하면 오류가 나지 않을까?
mov [esi],s
그래서 위와 같이 바꿔보았다.
operation size not specified???
구글에 검색해보니 프로세서는 메모리에서 메모리로 이동시키는 instruction이 없어서 하나를 거쳐서 가야된다는 뜻으로 추정되는 영어들이 나타났다. 그래서
쓰지 않는 rdx 에 문자열 s를 대입하고 그걸 다시 esi에 대입하니 오류가 나지 않았다!!!!! 그래서 나는
'이거 진짜 되는거 아니야..?'라는 헛된 꿈과 함께 마지막
./strlenex
를 터미널에 입력했다. 결과는
진짜 뭐가 문젤까?
코드에 혁명을 일으켜봤다.
결과는 똑같았다. segmentation fault(core dumped)
코드를 바꾸기 전에 저게 뭐하는 새끼인지부터 알아봐야 할 것 같다.
뭐 cpu에게 프로그램이 종료되었음을 알려주지 않았다~뭐 이런 얘기가 많아서 sys_exit을 포함해봤다.
이를 실행해보니!?
출력 없이 그냥 끝나버린다. 흐으으음 출력에 문제가 있는 것인가
출력 부분을 한 번 살펴보자
원래는 rax에 1을 넣어서 sys_write를 사용했었지만 오류가 계속 나서 위와 같이 바꿨었다. 혹시 이것때문은 아닐까 하는 희망을 가져보고 원래대로 고쳐보겠당!
위와 같이 바꾸고 다시 실행을 해보니
공백이 출력된다. 어..?
카운트가 되지 않은건가..?
오류를 잡았다,,내가 하도 오류메시지 좀 안뜨게 해달라고 빌어서 그런지 오류메세지는 뜨지 않는다.
대신 아무것도 뜨지 않는다.
생각을 해보면 현재 rcx는 0으로 초기화 되어있다. 출력이 정상적으로 이루어졌다면 0이라도 출력이 되었어야 하는데, 0도 출력이 되지 않는걸 보니!? 출력에 문제가 있다. 내가 고치고 만다. 1시간 타임어택 시작
진전이라고 봐야할까..? 26분이 지난 지금 세미콜론(;)을 출력하는데 성공했다.
도대체 저 세미콜론은 어디서 나온걸까..?
엄청난 수정을 거쳐 <를 출력해냈다.
<지금까지의 strlenex_r.asm>
_____________________________________________도저히 해결될거 같지 않아 재빠르게 다른 strlen 코드를 가져와보겠당.__________________________________
_strlen:
push rcx
xor rcx, rcx
_strlen_next:
cmp [rdi], byte 0
jz _strlen_null
inc rcx
inc rdi
jmp _strlen_next
_strlen_null:
mov rax, rcx
pop rcx
ret
_strlen | strlen을 시작! |
push rcx | 현재 저장되어 있는 rcx값을 push해놓음으로써 모두 다 끝난후 원래의 rcx값을 이용할 수 있게 한다 |
xor rcx,rcx | rcx를 0으로 초기화 |
_strlen_next: | 본격적으로 반복(loop) 되는 곳이다. |
cmp [rdi],byte 0 | 문자열이 저장되어 있는 주소인 rdi에 저장된 값과 byte 0(널 문자)와 비교를 진행한다 |
jz _strlen_null | 만약 비교를 진행하면서 널문자를 발견한다면 _strlen_null로 이동한다. |
inc rcx | 비교를 진행하며 널문자가 발견되지 않았다면 아직 문자열인 것이니, 문자열의 길이를 세는 레지스터인 rcx에 1을 더해준다 |
inc rdi | 문자열의 주소값을 저장하고 있는 rdi에 1을 더해줌으로써 다음 문자를 비교할 준비를 한당 |
jmp _strlen_next | 다시 _strlen_next로 복귀함으로서 비교 진행 |
mov rax,rcx | 문자열의 길이를 리턴하기 위해 rax에 문자열의 길이를 갖고있는 rcx의 값을 대입시킨다 |
pop rcx | 아까 초반부에 push 시켜놨던 rcx값을 pop으로 되찾아온다. |
ret | 문자열의 길이를 반환한다. |
위의 풀이가 제가 예상하고 원했던 흐름인데,,내 코드는 왜 정상작동을 거부하져
section .data
msg1 db 'destination string : ',0
msg2 db 'source string : ',0
section .bss
destination_string resb 1024
source_string resb 1024
section .text
global main
main:
xor rax,rax
xor rsi,rsi
xor rdi,rdi
xor rbx,rbx
xor rdx,rdx
mov rax,1
mov rdi,1
mov rsi,msg1
mov rdx,22
syscall
mov rax,0
mov rdi,0
mov rsi,destination_string
mov rdx,1024
syscall
mov rax,1
mov rdi,1
mov rsi,msg2
mov rdx,17
syscall
mov rax,0
mov rdi,0
mov rsi, source_string
mov rdx,1024
syscall
mov rdi,destination_string
mov rsi, source_string
call _strcat
mov rax,1
mov rdi,1
mov rsi,destination_string
mov rdx,1024
syscall
mov rax,60
mov rdi,0
syscall
section data | 초기화된 데이터들을 저장하는 data 섹션에 출력할 문자열들을 저장해놓았다. |
msg1 db 'destination string : ',0 | |
msg2 db 'source string : ',0 | |
section .bss | 초기화 되지 않은 데이터들을 저장하는 bss 섹션에 이음을 받을 문자열과 이을 문자열을 입력받는 메모리를 할당해놨다 |
destination_string resb 1024 | |
source_string resb 1024 | |
main: | 메인은 문자열을 출력하고 입력받고 하는거 뿐이여서 설명 생략. |
_strcat:
push rbp
mov rbp,rsp
xor r9,r9
xor r10,r10
xor rax,rax
jmp _find_memory
_find_memory:
mov al,[destination_string+r10]
cmp al,10
je _write_data
add r10,1
jmp _find_memory
_write_data:
xor rax,rax
mov al,[rsi+r9]
mov [destination_string+r10],al
cmp al,0
je _strcat_end
inc r9
inc r10
jmp _write_data
_strcat_end:
leave
ret
_strcat: | 본격적인 strcat 구현 시작 |
push rbp | rbp에 저장되어 있는 값을 스택에 저장 |
mov rbp,rsp | rbp에 rsp의 값을 대입 |
xor r9,r9 | r9 0으로 초기화 |
xor r10,r10 | r10 0으로 초기화 |
xor rax,rax | rax 0으로 초기화 |
jmp _find_memory | _find_memory로 이동 |
_find_memory: | |
mov al,[destination_String+r10] | al에 이음을 받을..?당할..? 문자열의 주소에 r10을 더한 그 주소에 저장되어 있는 값을 대입한다. |
cmp al,10 | al과 10을 비교한다. 10은 개행의 의미 |
je _write_data | 개행 기호를 만나면 _write_data로 이동한다 |
add r10,1 | r10에 1을 더해줌으로써 다음 비교 준비 |
jmp _find_memory | _find_memory로 다시 복귀함으로써 비교 시작 |
_write_data: | 이어붙이는 곳,, |
xor rax,rax | rax 0으로 초기화 |
mov al,[rsi+r9] | al에 rsi(이어붙일 문자열)+r9의 주소값에 저장된 값을 대입 |
mov [destination_string+r10],al | 이어붙임을 당할 문자열 끝에 위에서 al에 대입받은 문자 이어붙임 |
cmp al,0 | 입력받은 이어붙일 문자열이 널문자를 만난다면 strcat 끝 |
je _strcat_end | 프로그램 종료시키러 이동 |
inc r9 | 다음 문자를 받기 위해 1을 더함 |
inc r10 | 이어붙임을 당할 문자열도 다음 주소에 대입해야되기 때문에 1을 더함 |
jmp _write_data | 다음 문자 이어붙이러 위로 이동 |
_strcat_end: | 종료 시키기 |
leave | 종료 |
ret |
과제로 나온 함수 하나정도는 직접 구현해보고 싶다는 욕심에 많이 늦어져서 죄송합니다.
댓글
이 글 공유하기
다른 글
-
지옥방 3주차 과제 달고나 문서 뿌시기!
지옥방 3주차 과제 달고나 문서 뿌시기!
2020.04.23 -
지옥방 3주차 과제 DreamHack-System Exploitation Fundamental
지옥방 3주차 과제 DreamHack-System Exploitation Fundamental
2020.04.21 -
지옥방 웹해킹 1주차 과제
지옥방 웹해킹 1주차 과제
2020.03.30 -
웹해킹 1주차 PHP
웹해킹 1주차 PHP
2020.03.30