윈도우팁

게시글 '어셈블리 강좌'에 대한 정보
어셈블리 강좌
등록일 2005-10-14 조회 4,313
1 어셈블리 1.1 머릿말 1.2 어셈블리 개요 1.2.1 어셈블리의 필요성 1.3 디지털 이론 1.4 어셈블리 기초접근 1.4.1 Assemble(어셈블) 1.4.2 Link(링크) 1.4.3 Register(레지스터) 1.4.4 주소지정방식 1.4.5 AT&T 문법 1.4.6 Intel 문법 1.4.7 OP code 표 활용 1.4.8 포인터란 ? 1.4.9 포인터 참조 1.4.10 배열과 포인터 1.4.11 함수포인터 1.4.12 다차원 포인터 1.4.13 링크드 리스트 1.4.14 생각해볼 문제 1.5 명령어 정리 (x86) 1.5.1 AAA (ASCII adjust for addtion: BCD의 가산 보정) 1.5.2 AAD (ASCII adjust for division: BCD의 나눗셈 보정) 1.5.3 AAM (ASCII adjust for multiply: BCD의 곱셈 보정) 1.5.4 AAS (ASCII adjust for subtraction: BCD의 뺄셈 보정) 1.5.5 ADC (Add with carry: Carry flag 와 함께 덧셈) 1.5.6 ADD (Addition: 덧셈) 1.5.7 AND (AND: 논리연산 AND) 1.5.8 CALL (Call a procedure: 구역반환 호출) 1.5.9 CBW (Converrt byte to word: 바이트를 워드로 확장) 1.5.10 CLC (Clear carry flag: Carry flag 0으로 초기화) 1.5.11 CLD (Clear direction flag: Direction flag 를 순방향을 뜻하는 0으로 초기화) 1.5.12 CLI (Clear interrupt flag: 인터럽트 금지) 1.5.13 CMC (Complement carry flag: Carry flag 반전) 1.5.14 CMP (Compare: 비교연산) 1.5.15 CMPS (Compare string: 메모리간의 비교연산) 1.5.16 CWD (Convert word to double word: 워드를 더블워드로 확장) 1.5.17 DAA (Decimal adjust for addtion: BCD간의 덧셈결과를 pack BCD형식으로 변환) 1.5.18 DAS (Decimal adjust for subtraction: BCD간의 뺄셈결과를 pack BCD형식으로 변환) 1.5.19 DEC (Decrement destination by 1: 단위뺄셈) 1.5.20 div (Divide: 부호고려없는 나눗셈) 1.5.21 ESC (Escape: 외부 명령어 전달) 1.5.22 HLT (Halt: 인터럽트 초기화전까지 정지) 1.5.23 Idiv (Integer division signed: 부호를 고려한 나눗셈) 1.5.24 IMUL (Integer multiply: 부호를 고려한 곱셈) 1.5.25 IN (Input port: 포트로부터 어큐물레이터로 입력) 1.5.26 INC (Increment destination by 1: 단위덧셈) 1.5.27 INT (Interrupt: S/W 인터럽트 발생) 1.5.28 INTO (Interrupt if overflow: Overflow시에 인터럽트 4번 벡터 발생) 1.5.29 IRET (Interrupt return: 인터럽트로부터 복귀) 1.5.30 JA (Jump if above: > 이면 분기) 1.5.31 JAE (Jump if above or equal: >= 이면 분기) 1.5.32 JNBE (Jump if not below nor equal: <= 이 아니면 분기) 1.5.33 JNB (Jump if not below: < 이 아니면 분기) 1.5.34 JB (Jump if below: < 이면 분기) 1.5.35 JNAE (Jump if not above nor equal: >= 이 아니면 분기) 1.5.36 JBE 1.5.37 JNA 1.5.38 JCXZ 1.5.39 JE 1.5.40 JZ 1.5.41 JG 1.5.42 JNLE 1.5.43 JGE 1.5.44 JNL 1.5.45 JL 1.5.46 JGNE 1.5.47 JLE 1.5.48 JNG 1.5.49 JMP 1.5.50 JNE 1.5.51 JNZ 1.5.52 JNO 1.5.53 JNP 1.5.54 JPO 1.5.55 JNS 1.5.56 JO 1.5.57 JP 1.5.58 JPE 1.5.59 JS 1.5.60 LAHF (Load AH from 8080 flags: 8080 호환 flags를 AH에 읽어들임) 1.5.61 LDS (Load DS with register: DS와 레지스터에 읽어들임) 1.5.62 LEA (Load effective address: 오프셋 주소를 읽어들임) 1.5.63 LES (Load ES with register: ES와 레지스터에 읽어들임) 1.5.64 LOCK (Lock bus: Memory bus 일시적 공유제한) 1.5.65 LODS (Load string: 메모리 읽음) 1.5.66 LOOP (Loop: 반복분기) 1.5.67 LOOPZ (Loop if zero: 0인경우와 함께 반복분기) 1.5.68 LOOPE (Loop if equal: 비교결과 같은경우와 함께 반복분기) 1.5.69 LOOPNZ (Loop if not zero: 0이 아닌경우와 함께 반복분기) 1.5.70 LOOPNE (Loop if not equal:비교결과 다른경우와 함께 반복분기) 1.5.71 MOV (Move: 전송) 1.5.72 MOVS (Move string: 메모리간 이동) 1.5.73 MUL (Multiply: 부호를 고려하지 않는 곱셈) 1.5.74 NEG (Negate: 2의 보수로 변환) 1.5.75 NOP (No operation: 의미없는 실행) 1.5.76 NOT (Not: 1의 보수로 변환, 비트반전효과) 1.5.77 OR (OR: 논리연산 OR) 1.5.78 OUT (Out port: 어큐물레이터로부터 포트로 출력) 1.5.79 POP (Pop stack into destination: 스택에서 꺼냄) 1.5.80 POPF (Pop stack into flags: 스택에서 꺼내어 flags로 저장) 1.5.81 PUSH (Push to stack: 스택에 밀어넣기) 1.5.82 PUSHF (Push flags to stack: flags를 스택에 저장) 1.5.83 RCL (Rotate through carry left: 캐리와 함께 왼쪽으로 비트회전) 1.5.84 RCR (Rotate through carry right: 캐리와 함께 오른쪽으로 비트회전) 1.5.85 REP (Repeat: 스트링명령 반복) 1.5.86 REPZ (Repeat if zero: 0인동안 스트링명령 반복) 1.5.87 REPE (Repeat if equal: 비교결과가 같은동안 스트링명령 반복) 1.5.88 REPNZ (Repeat if not zero: 0인 아닌동안 스트링명령 반복) 1.5.89 REPNE (Repeat if not equal: 비교결과가 다른동안 스트링명령 반복) 1.5.90 RET (Return from procedure: 구역복귀) 1.5.91 ROL (Rotate left: 죄측으로 비트회전) 1.5.92 ROR (Rotate right: 우측으로 비트회전) 1.5.93 SAHF (Store AH into 8080 flags: AH값을 8080호환 flags 로 저장) 1.5.94 SAL (Shift arithmetic left: 좌측 산술 비트이동) 1.5.95 SHL (Shift left: 좌측 비트이동) 1.5.96 SAR (Shift arithmetic right: 우측 산술 비트이동) 1.5.97 SBB (Subtract with below: 자리빌림을 고려한 뺄셈) 1.5.98 SCASB (Scan string: 메모리 검색) 1.5.99 SHR (Shift right: 우측 비트이동) 1.5.100 STC (Set carry flag: Carry flag를 1로 전환) 1.5.101 STD (Set direction flag: Direction flag를 역방향을 가르키는 1로 전환) 1.5.102 STI (Set interrupt flag: 인터럽트 개시) 1.5.103 STOS (Store string: 메모리로의 저장) 1.5.104 SUB (Subtract: 뺄셈) 1.5.105 TEST (Test: bit 검사) 1.5.106 WAIT (Wait: 동기화 대기) 1.5.107 XCHG (Exchange: 교환) 1.5.108 XLAT (Translate: 간접 상대 전송) 1.5.109 XOR (Exclusive OR: 배타적 OR) 1.6 명령어 정리 (i386 추가분) 1.7 명령어 정리 (Pentium 추가분) 1.8 명령어 정리 (MMX/SSE/3DNOW) 1.9 명령어 정리 (8051) 1.10 명령어 정리 (PIC) 1.11 어셈블리 활용 1.12 어셈블리 기교 1.13 역어셈블 (Reverse assemble) 1.13.1 바이러스(Virus) 대응 백신(Vaccine) 제작 1.14 부록 1.15 마치면서 1.16 질문 및 답변 1.1 머릿말 # 이 문서는 개인적으로 책출판 의뢰가 들어와서 정리했던 것을 출판을 포기하고 재 정리한 것입니다. 언젠가 다시 출판을 위해서 다시 새롭게 작성할것이지만 우선은 그러지 않기로 했습니다. 여기서 제시되는 예제는 모두 Linux환경의 GNU assembler(GAS)와 DOS/Win32환경의 Turbo/Macro assembler를 기준으로 작성되었습니다. 또한 특별히 언급이 없다면 Little endian을 기준으로 설명합니다. 작성일자: 2004년 5월 8일 부터 1.2 어셈블리 개요 # 어셈블리를 주변에서 들어본 사람이 있을겁니다. 대부분 아마도 어렵다고 예기를 들었을겁니다. 하지만 그것은 잘못된 표현이라고 확신합니다. 어셈블리는 다른 언어와 비슷한 학문입니다. 어렵다기보다는 번거롭다라는 예기가 적절한 표현이 아닐까 생각합니다. 분명 어셈블리는 어렵지 않습니다. 단지 번거로울뿐입니다. 그리고 어셈블리는 번거롭기 때문에 언어로서의 장점을 가지고 있습니다. 번거롭다는 것은 보다 세밀하게 기술해야 되기 때문이며 불필요한 작업이 많아서 번거로운것이 아닙니다. 1.2.1 어셈블리의 필요성 # 오래전부터 수많은 프로그래밍 언어들이 존재하였지만 어떤 언어는 사라져가는것이 있고 어떤 언어는 첫 탄생부터 지속적으로 잊혀지지 않는 언어가 있습니다. 보통은 결과물의 실행속도 및 크기 그리고 개발의 편리성(효율성)이 그 언어의 지속적인 사랑을 유지할수 있게 합니다. 어셈블리는 개발의 편리성에서는 다소 약한 모습을 보이기는 하지만 실행속도와 크기면에서 그 어떠한 언어보다 우수할수 있는 자리매김을 하는 언어입니다. 물론 요즘의 C언어나 Java 같은경우 굉장히 우수한 성능을 발휘하도록 발전하였습니다. 하지만 분명한것은 어셈블리로 작성하였을때 적어도 다른언어보다 성능이 같거나 뛰어날수 있다는 점입니다. 하드웨어의 발전이 해를 거듭할수록 눈부신 성장을 하고 있지만 아직은 어셈블리의 활용을 필요로 하는 분야가 남아있습니다. 앞으로도 1세기동안은 활용분야가 많을것이라는 예측을 필자는 하고 있습니다. 예를 들어서 멀티미디어관련 영상처리분야는 어셈블리의 의존도가 상당히 높은분야임이 확실합니다. 그래서 아직도 하드웨어로 영상처리를 하는 경우도 많이 있습니다. 높은 하드웨어 성능의 발전에 따라서 그만큼 처리량도 비례적으로 함께 증가하는게 추세이며 이것은 곧 눈부신 하드웨어 성능의 발전과 더불어 항상 부족한 소프트웨어의 처리성능이 뒤따른다는 점입니다. 결국 이러한 경우가 굉장히 많다고 볼수는 없지만 그래도 항상 필요할때가 있는 언어인것이 어셈블리입니다. 1.3 디지털 이론 # 진법 2진수 산술연산 논리연산 연산의 특징 1.4 어셈블리 기초접근 # 1.4.1 Assemble(어셈블) # 1.4.2 Link(링크) # 1.4.3 Register(레지스터) # 1.4.4 주소지정방식 # Direct addressing (직접 주소 대입) Double indexed addressing (레지스터간 상대 주소 대입) Immediate addressing (상수 대입) Register addressing (레지스터 대입) Register indirect addressing (레지스터 간접 주소 대입) Single indexed addressing (레지스터 상대 주소 대입) Stack addressing (스택 주소 대입) 1.4.5 AT&T 문법 # 1.4.6 Intel 문법 # 1.4.7 OP code 표 활용 # 1.4.8 포인터란 ? # 프로그래밍의 커다란 요소로는 중앙처리장치/메모리/주변기기 를 효과적인 알고리즘을 사용하여 다루는 것이라고 예기할수 있다. 여기서 포인터는 메모리를 어떻게 적은 비용으로 접근할것인가에 대한 해결책이다. 그렇다면 방대한 메모리에서 특정영역에 자신이 원하는 자료가 있다면 이곳에 있는 값을 어떻게 가져올것인가? 예를 들어보면 멀리 떨어져 있는 사람이 나와 통화하려면 전화번호가 필요하듯이 메모리에도 그러한 숫자(주소)를 정해놓았다. 우리가 원하는 자료가 있는 곳을 0x0010이라는 주소로 정해두었다면 "0x0010에서 1바이트의 자료를 읽어라" 라는 명령을 사용하여 자료를 읽어올수 있다. 여기서 0x0010이라는 주소가 자료로 재해석이 될수 있게 된다. 우리는 여기서 자료를 가르키는 주소값을 포인터라고 예기한다. 그리고 이 주소를 담고 있는 메모리 또는 레지스터를 포인터 변수라고 예기한다. C언어에서는 포인터를 "void *MyPointer=&MyValue;"와 같이 "*"(포인터 기호)와 "&"(주소참조 기호)를 사용하여 표현한다. 하지만 어셈블리에서는 포인터에 대한 특별한 선언방식이 없다. 단지 변수를 선언하고 그곳에 값을 넣어서 어떻게 다루는가에 따라서 그것이 포인터라고 예기할수 있을뿐이다. 굳이 C언어와 어셈블리간의 기호의 차이를 비교한다면 "*"는 "[]"로 해석되고 "&"는 "OFFSET" 또는 "LEA"이라는 것으로 해석된다고 일단 믿어보자. 나중에 이것은 어셈블리가 보다 많은 것을 내포하고 있다는 것을 이해할 준비를 하고 있기만 하다면 그대로 믿어보자. C언어에서의 포인터 시작 >>> unsigned char MyValue = 0; unsigned char *MyPointer = &MyValue; <<< 끝 어셈블리에서의 포인터 (MASM/TASM) 시작 >>> MyValue DB 0 MyPointer DD OFFSET MyValue <<< 끝 일단 위와 같이 이번장에서는 특별한 언급이 없다면 혼돈을 막기 위해서 MASM(Macro assembler) 또는 TASM(Turbo assembler)가 사용하는 문법만 적도록 하겠다. 하지만 어셈블리는 문법이 다양하므로 이점도 유의하면서 읽어가기 바란다. (필자가 간간히 GAS(GNU assembler)문법도 소개할것이다. 그렇다고 어셈블리 명령어를 각각 따로 배워야 하는것은 아니다. 기본적으로 명령어의 성격은 같고 약간씩 단어선택의 차이와 위치만 차이날 뿐이다.) 독자는 이제 "포인터를 사용하면 어디가 좋다는 것이가?"라는 질문을 하고 싶을것이다. 일단 구체적인것은 차츰 읽어가면서 이해할수 있기를 바라면서 간략히 다음과 같은 활용분야가 있다는 것을 상기하자. 많은 자료를 함수(Function 또는 Procedure)로 넘겨줄때 실제 자료를 넘겨주지 않고 그 위치를 가진 변수(포인터 변수)를 넘겨주어 자료의 이동을 최소화 한다. 방대한 자료를 추가(삽입) 또는 삭제할때 적은 비용으로 행할수 있다. (링크드 리스트의 활용) 가변적인 크기를 가진 자료를 다루는데 효과적이다. (할당 및 해제) 1.4.9 포인터 참조 # 포인터는 그 자체가 자료를 나타내지 않지만 자료가 위치한 주소를 나타낸다고 언급하였다. 그렇다면 자료로 접근하는 것은 어떻게 이루어질까? 우선 주소에 "["와 "]"로 감싸면 그 주소가 가르키는 곳의 자료를 뜻한다. 하지만 주소라는 것이 위치만 가르킬뿐 실제 자료의 크기를 명시하지 않는다면 얼마나 자료가 있는지 알수가 없다. 때문에 "BYTE PTR", "WORD PTR", "DWORD PTR", "QWORD PTR" 등의 수식어가 붙거나 명령어 자체에서 구분되거나 함께 사용된 레지스터의 크기에 의해서 자동으로 명시될수 있다. 아래의 예제를 보면 MyValue라는 1바이트 자료가 있다. 이것을 MyPointer가 그 주소(오프셋)를 가지고 포인터 변수 역할을 하며 이 포인터 변수에서 주소값을 EBX에 읽어들이고 EBX가 가르키는 주소를 참조하여 "BYTE PTR"이라는 수식어로 1바이트만 1로 바꾸게 된다. 이처럼 포인터 변수를 통해서 자료로 접근하는 것을 포인터 참조라고 말한다. 시작 >>> .MODEL FLAT CODE SEGMENT ; ... MOV EBX, DWORD PTR MyPointer ; 여기서 EBX레지스터는 DWORD크기로 명시적인 크기를 나타내므로 DWORD PTR은 생략 가능하다. MOV BYTE PTR [EBX], 1 ; 여기서 '1'이라는 숫자는 몇바이트인지 알수 없다. 때문에 BYTE PTR이라는 것은 생략이 불가능하다. ; ... CODE ENDS DATA SEGMENT MyValue DB 0 ; 1바이트 자료선언 MyPointer DD OFFSET MyValue ; MyPointer는 MyValue의 주소(32bit 오프셋)를 가지며 선언되었다. DATA ENDS <<< 끝 C언어에서는 위의 MyPointer가 가르키는 MyValue가 1바이트 이므로 "char *"또는 "unsigned char *"로 선언되었을것이라는 것을 예측할수 있다. 다음과 같이 생각을 해도 틀리지 않은 생각이다. "char *"로 선언된 것은 "BYTE PTR" 이 수식어로 우선적으로 선택되어진다. (강제로 캐스팅되지 않는다면) "int *" 로 선언된 것은 "DWORD PTR" (16bit 운영체제에서는 "WORD PTR")이 수식어로 우선적으로 선택되어진다. (강제로 캐스팅되지 않는다면) "long *"로 선언된 것은 "DWORD PTR" 이 수식어로 우선적으로 선택되어진다. (강제로 캐스팅되지 않는다면) 시작 >>> DATA SEGMENT MyValue_char DB 0 ; char MyValue_char = 0; MyValue_short DW 0 ; short MyValue_short = 0; MyValue_int DD 0 ; int MyValue_int = 0; MyValue_long DD 0 ; long MyValue_long = 0l; MyPointer_char DD OFFSET MyValue_char ; char *MyPointer_char = &MyValue_char; MyPointer_short DD OFFSET MyValue_short ; short *MyPointer_short = &MyValue_short; MyPointer_int DD OFFSET MyValue_int ; int *MyPointer_int = &MyValue_int; MyPointer_long DD OFFSET MyValue_long ; long *MyPointer_long = &MyValue_long; DATA ENDS <<< 끝 1.4.10 배열과 포인터 # 배열과 포인터의 차이점 일단 C언어에서 다음과 같은 소스를 보았을때 배열과 포인터의 차이점을 이해할수 있다면 다음 부분으로 넘어가도 좋을듯 하다. <그림 1> 시작 >>> int main(void) { char ArrayString[] = "Hello world"; /* 배열선언 및 문자열로 초기화 */ char *PointerString = "Hello world"; /* 포인터 변수 선언 및 문자열 상수의 주소 대입 */ printf("%s == %s ?\n", ArrayString, PointerString); } <<< 끝 이제 이것을 어셈블리로 바꿔서 한번 관찰해보자. 시작 >>> .MODEL FLAT extrn printf CODE SEGMENT main PROC NEAR PUSH EBP MOV EBP, ESP ; ESP 저장 SUB ESP, 4 ; char *PointerString; /* 스택으로부터 자동변수 확보 */ MOV DWORD PTR [EBP - 4], OFFSET L_TEMPLABEL_CONST_STRING_01__ ; PointerString = &L_TEMPLABEL_CONST_STRING_01__[0]; PUSH DWORD PTR [EBP - 4] ; PointerString PUSH OFFSET ArrayString ; &ArrayString[0] PUSH OFFSET L_TEMPLABEL_CONST_STRING_00__ ; "%s == %s ?\n" CALL printf ADD 4 + 4 + 4 MOV ESP, EBP ; ESP 복원 POP EBP RET main ENDP CODE ENDS DATA SEGMENT ; 컴파일러에 의해서 라벨이 관리되는 영역 L_TEMPLABEL_CONST_STRING_00__ DB "%s == %s ?\n" L_TEMPLABEL_CONST_STRING_01__ DB "Hello world" ; 초기화된 변수영역 ArrayString DB "Hello world" DATA ENDS <<< 끝 여기에서 배열선언은 ArrayString이라는 라벨명이 직접 사용되지만 PointerString은 컴파일러 내부에서 임의로 부여된 "L_TEMPLABEL_CONST_STRING_01__"이 선언되고 이곳의 주소를 가지고 있게 된다. (여기서 라벨명 L_TEMPLABLE_CONST_STRING_xx__은 컴파일러가 결정짓는 임의의 라벨명이니 라벨명자체에 크게 신경쓰지 말자.) 그리고 PointerString의 자료형은 DWORD의 크기를 선언하였다. 이것이 의미하는 것은 주소지정이 32bit라는 것을 알수 있으며 포인터의 크기가 32bit로 사용되었음을 알수 있다. 도스(16bit 운영체제)에서는 포인터의 자료형은 WORD의 크기를 갖는 세그먼트와 WORD의 크기를 갖는 오프셋으로 구성된다. 하지만 요즘 대부분의 운영체제(32bit 운영체제)는 DWORD를 오프셋으로 사용하며 세그먼트는 잘 사용하지 않는다. (참고: 커널내부에서는 셀렉터와 오프셋이 함께 사용되기도 한다.) 이제 조금 깊은 접근을 해보자. 배열이 아래와 같이 사용되었다. 여기서 a[5]는 값 6을 가지고 있다. 시작 >>> int main(void) { int a[10] = {1,2,3,4,5,6,7,8,9,10}; printf("a[5]=%d\n", a[5]); return(0); } <<< 끝 위의 소스를 포인터로 접근해보자. 시작 >>> int main(void) { int a[10] = {1,2,3,4,5,6,7,8,9,10}; int *b = &a[5]; printf("a[5] = *b =%d\n", *b); return(0); } <<< 끝 b 포인터는 a의 6번째 요소에 대한 주소값을 갖도록 의도되었다. 그리고 b에 담겨진 a[5]의 주소를 참조하여 값 6을 얻어올수 있었다. 그렇다면 여기서 b[4]를 printf로 확인해보자. 시작 >>> printf("b[4]=%d\n", b[4]); <<< 끝 어떤값이 출력되는가? 우선 천천히 추적해보자. 먼저 b는 a[5]의 주소를 가지고 있다. 그리고 b의 첨자로는 4라는 값이 사용되었다. 여기서 a와 b는 int형으로 자료형이 같으므로 a[5 + 4]로 해석될수 있게 된다. 그래서 결국은 a[9]를 나타내며 10이라는 숫자를 확인할수 있게 된다. 이제 이것을 어셈블리로 알아보자. 시작 >>> .MODEL FLAT CODE SEGMENT MOV EBX, OFFSET a ; EBX = &a[0] LEA EAX, [EBX + (5 * 4)] ; EAX = &a[5] /* 여기서 4를 곱한 이유는 배열 첨자의 상대적인 주소는 int의 자료형 크기에 비례하여 증가하기 때문이다. */ MOV DWORD PTR b, EAX ; b = EAX = &a[5] LEA EBX, b[4 * 4] ; 마찬가지로 int의 자료형의 크기인 4를 곱하였다. MOV EAX, [EBX] ; EAX = *b = [EBX] ; ... printf CODE ENDS DATA SEGMENT a DD 1,2,3,4,5,6,7,8,9,10 b DD ? DATA ENDS <<< 끝 1.4.11 함수포인터 # 함수포인터는 함수의 오프셋을 갖는 포인터이다. 하지만 이것은 다른 포인터와는 다르게 CALL명령에 의해서 함수처럼 사용되어질수 있게 된다. 물론 일반 포인터도 함수포인터로 캐스팅과정을 거치면 함수포인터로 취급받게 된다. 다음의 예제를 보자. 시작 >>> #include int MyFunction(int x, int s_y) { return( x + y ); } int main(void) { int a, b; int (*Sum)(int, int) = MyFunction; a = 1, b = 2; printf("%d + %d = %d\n", a, b, Sum(a, b)); return(0); } <<< 끝 위에서 보듯이 MyFunction이라는 함수를 Sum이라는 함수포인터에 대입한뒤에 마치 MyFunction을 호출하는것처럼 Sum을 같은 방식으로 호출한것을 볼수 있다. 결과는? 예상한 결과대로 잘 실행될것이다. 이제 이것을 어셈블리로 해석해보자. 시작 >>> .MODEL FLAT CODE SEGMENT MyFunction PROC NEAR MOV EAX, DWORD PTR [ESP + 8] ; y ADD EAX, DWORD PTR [ESP + 4] ; x RET ; return( x + y ) MyFunction ENDP main PROC NEAR PUSH EBP MOV EBP, ESP ; ESP 백업 SUB ESP, 4 + 4 + 4 ; int a, b; int (*Sum)(int, int); /* 자동변수 a, b Sum을 stack에서 확보 */ MOV DWORD [EBP - 4], 1 ; a = 1 MOV DWORD [EBP - 8], 2 ; b = 2 MOV DWORD [EBP - 12], OFFSET MyFunction ; Sum = &MyFunction PUSH DWORD [EBP - 8] ; b PUSH DWORD [EBP - 4] ; a CALL DWORD PTR [EBP - 12] ; EAX = Sum(a, b) ADD 4 + 4 PUSH EAX ; Sum(a,b) PUSH DWORD [EBP - 8] ; b PUSH DWORD [EBP - 4] ; a PUSH OFFSET L_TEMPLABEL_CONST_STRING_00__ CALL printf ADD 4 + 4 + 4 + 4 MOV ESP, EBP ; ESP 복원 POP EBP XOR EAX, EAX ; return(0) RET main ENDP CODE ENDS DATA SEGMENT L_TEMPLABEL_CONST_STRING_00__ DB "%d + %d = %d\n" DATA ENDS <<< 끝 일단 실제 컴파일러가 생성한 코드와 역간의 차이가 있을수 있지만 대략 위와 같은 코드가 될것이다. (C 컴파일러의 종류와 최적화 옵션에 따라서 생성된 코드는 크게 영향을 받는다.) 위에서 보듯이 어셈블리에서는 함수포인터도 DWORD 변수와 다를바 없다. 다만 C의 함수포인터는 "CALL DWORD PTR [x]"로서 해석되어진다는 것이 어셈블리에 보다 깊은 이해가 필요하다. 즉, 지금까지의 예제를 보듯이 용도가 어떤것이냐가 중요한것이지 선언이 중요한것은 아니라는 것이 어셈블리의 관점이다. 1.4.12 다차원 포인터 # 다차원 포인터는 포인터가 포인터를 가르키는 중첩된 포인터라고 설명하는것이 간결하고 많은 것을 내포한 설명이 되지 않을까 생각된다. 우리는 일단 2차원 포인터만을 생각하자. 그리고 3차원 포인터는 그것의 응용이라는 점을 고려하면 이해가 빠를것이다. <그림 2> 위의 그림과 아래의 C소스는 간단히 a라는 변수를 b 포인터가 가르키고 다시 그 b 포인터를 c 포인터가 가르키는 구조를 나타낸다. 시작 >>> int main(void) { int a = 0; int *b = &a; int **c = &b; printf("a=%d, *b=%d, **c=%d\n", a, *b, **c); return(0); } <<< 끝 위의 소스는 생각보다 쉽게 접근하기는 어렵다. 이것을 어셈블리로 시점을 바꿔서 보자. 시작 >>> .MODEL FLAT CODE SEGMENT main PROC NEAR PUSH EBP MOV EBP, ESP SUB 4 + 4 + 4 ; int a, *b, **c; MOV DWORD PTR [EBP - 4], 0 ; a = 0; LEA EBX, DWORD PTR [EBP - 4] ; EBX = &a; MOV DWORD PTR [EBP - 8], EBX ; b = EBX = &a; LEA EAX, DWORD PTR [EBP - 8] ; EAX = &b; MOV DWORD PTR [EBP - 12], EAX ; c = EAX = &b; MOV EAX, DWORD PTR [EAX] ; EAX = &b = *c; /* 한단계 포인터 참조 */ MOV EAX, DWORD PTR [EAX] ; a = **c = *(&b) = EAX; /* 다시 한단계 포인터 참조 : 결국 a값을 얻어냄 */ PUSH EAX ; **c; PUSH DWORD PTR [EBX] ; *b; PUSH DWORD PTR [EBP - 4] ; a PUSH OFFSET L_TEMPLABEL_CONST_STRING_00__ CALL printf ADD 4 + 4 + 4 + 4 MOV ESP, EBP POP EBP RET main ENDP CODE ENDS DATA SEGMENT L_TEMPLABEL_CONST_STRING_00__ DB "a=%d, *b=%d, **c=%d\n" DATA ENDS <<< 끝 여기서 "LEA"라는 명령이 하는 일은 무엇일까? "LEA EBX, [EBP]" 이라고 하였을때 이것은 "MOV EBX, EBP"와 같다. 그러나 이러한 경우는 "LEA"명령을 잘 사용하지 않는다. 위에서 보듯이 [EBP - 8] 처럼 EBP만 있는것이 아니고 EBP로부터 상대적인 거리값 -8과 같은 상대주소값이 있을때 그 기능이 잘 활용된다. 만약 단순히 [EBP]를 포인터 참조한다면 그냥 "MOV EBX, EBP"를 통해서 포인터값을 얻어오고 "MOV EAX, [EBX]"로 값을 참조하는게 좋다. 다차원 포인터는 보통 2차원포인터까지가 많이 사용되며 그 이상의 다차원은 잘 사용되지 않는다. 이유는 C언어로 구현하더라도 프로그래머가 생각을 많이 해야 겨우 버그를 피할수 있기 때문에 유용한 면보다 잃어버릴수 있는 부분이 크다고 해도 과언은 아니다. 물론 다차원이 머리속에서 정확히 그려지는 사람이라면 잘 활용하여 굉장한 동작을 구현할수도 있겠지만 다른사람이 그 코드를 봤을때 이해도가 떨어지게 되어 신중하게 선택되어야 할 개념중에 하나라고 당부하고 싶다. 1.4.13 링크드 리스트 # 1.4.14 생각해볼 문제 # "소스 1"과 "소스 2"와의 차이점이 있을까? 차이점이 있다면 어떤것일까? 반대로 차이가 없다면 왜 없을까? 소스 1 시작 >>> #include int main(void) { char ArrayString[] = "Hello world"; printf("%s\n", ArrayString); return(0); } <<< 끝 소스 2 시작 >>> #include int main(void) { char ArrayString[] = "Hello world"; printf("%s\n", &ArrayString[0]); return(0); } <<< 끝 다음의 소스에서 빈칸에 적절한 내용을 적어보자. 시작 >>> DATA SEGMENT MyValue_long ( 빈칸 ) 0 ; int MyValue_long = 0; MyValue_char ( 빈칸 ) 0 ; char MyValue_char = 0; MyPointer ( 빈칸 ) OFFSET MyValue_char ; void *MyPointer = (void *)(&MyValue_char); DATA ENDS <<< 끝 1.5 명령어 정리 (x86) # x86 명령어는 약 100여개 정도입니다. 왜운다기 보다는 어떤 의미의 단어에서 축약되었는지를 파악하는게 보다 쉽게 이해할수 있을겁니다. 1.5.1 AAA (ASCII adjust for addtion: BCD의 가산 보정) # 중요도: 매우낮음 설명: 덧셈결과 저장된 AL의 값을 취하여 이를 BCD로 변환합니다. (즉, 덧셈결과의 2진값을 BCD로 변환) OPCODE: 00110111 Flag: AF, CF 의사표현 시작 >>> if( ((AL & 0x0f) > 9) || (AF == 1) ) { AL = AL + 6; AH = AH + 1; CF = AF = 1; AL = AL & 0x0f; } <<< 끝 예제 시작 >>> MOV AX, 0708H ; BCD로는 78D 가 되겠습니다. ADD AL, 02H ; AL + 02H = 08H + 02H = 0AH AAA ; AH/AL=07H/0AH 로 저장된 AX값을 명령 수행결과 0800H로 변환되며 BCD값인 80D로 해석될수 있음 <<< 끝 활용: 이것은 일부 BCD를 사용하는 BIOS의 값을 처리하는 분야와 컴파일러 개발자들에게 활용의 범위가 제한되어 보입니다. 필자도 이것을 실제 사용해본적은 BIOS의 시간정보가 BCD로 되어 있어서 이를 처리하기 위해서 사용해본적이 있을뿐 그 외에는 한번도 없었습니다. 1.5.2 AAD (ASCII adjust for division: BCD의 나눗셈 보정) # 중요도: 매우낮음 설명: BCD수를 나누기 위해서 AX를 보정합니다. (보정공식: AX = AH * 0AH + AL) OPCODE: 11010101 00001010 Flag: PF, SF, ZF 의사표현 시작 >>> AL = AH * 0AH + AL; AH = 0; <<< 끝 예제 시작 >>> MOV AX, 0708H ; BCD로는 78D MOV DL, 02H ; 나눌수 02H AAD ; AX값을 BCD 78D인 0708H에서 나눗셈을 위한 보정수로 4EH(AH * 0AH + AL) 로 변환합니다. div DL ; 실제 연산되는 것은 4EH(AX) / 02H(DL) = 27H(39D) <<< 끝 활용: AAA명령과 비슷하게도 큰 활용분야는 없어보입니다. 1.5.3 AAM (ASCII adjust for multiply: BCD의 곱셈 보정) # 중요도: 매우낮음 설명: 곱셈결과 AX값을 BCD로 변환합니다. OPCODE: 11010100 00001010 Flag: PF, SF, ZF 의사표현 시작 >>> AH = AL / 0AH; AL = AL % 0AH; <<< 끝 예제 시작 >>> MOV AL, 07H ; AL=07H MOV DL, 05H ; DL=05H MUL DL ; AX = 0023H(35D) = 07H(AL) * 05H(DL) AAM ; AX = 0305D = BCD변환 <<< 끝 활용: AAA명령과 비슷하게도 큰 활용분야는 없어보입니다. 1.5.4 AAS (ASCII adjust for subtraction: BCD의 뺄셈 보정) # 중요도: 매우낮음 설명: BCD로 다루어진 뺄셈결과를 BCD로 보정합니다. OPCODE: 00111111 Flag: AF, CF 의사표현 시작 >>> if( ((AL & 0x0f) > 9) || (AF == 1) ) { AL = AL - 6; AH = AH - 1; CF = AF = 1; AL = AL & 0x0f; } <<< 끝 예제 시작 >>> MOV AX, 0708H ; AX=0708H (BCD로 해석하면 78D) SUB AL, 09H ; 09H를 빼면 AL=FFH이 되고 CF가 1로 변합니다. AAS ; AX=0609H 즉, BCD 69D가 됩니다. <<< 끝 활용: AAA명령과 비슷하게도 큰 활용분야는 없어보입니다. 1.5.5 ADC (Add with carry: Carry flag 와 함께 덧셈) # 중요도: 매우높음 설명: CF를 포함한 덧셈을 수행합니다. OPCODE(reg,reg): 0001000W 11(reg)(r/m) OPCODE(mem,reg): 0001000W (mod)(reg)(r/m) OPCODE(reg,mem): 0001001W (mod)(reg)(r/m) OPCODE(reg,imm): 100000SW 11010(r/m) OPCODE(mem,imm): 100000SW (mod)010(r/m) OPCODE(acc,imm): 0001010W Flag: AF, CF, OF, PF, SF, ZF 의사표현 시작 >>> /* ADC , */ To = To + From + CF; <<< 끝 예제 시작 >>> MOV AL, 07H ; AL=07H MOV DL, 08H ; DL=08H ADC AL, DL ; AL = AL + DL + CF <<< 끝 활용: 위의 예제에서 CF값이 덧셈에 관여된다는 것이 이 명령의 중요한 부분이며 뒤에 설명할 ADD명령과 상호 작용하는 덧셈확장의 의미로 매우 광범위하게 사용되며 때로는 CF값을 읽어오기 위해서 사용되기도 합니다. 1.5.6 ADD (Addition: 덧셈) # 중요도: 매우높음 설명: 덧셈 OPCODE(reg,reg): 0000000W 11(reg)(r/m) OPCODE(mem,reg): 0000000W (mod)(reg)(r/m) OPCODE(reg,mem): 0000001W (mod)(reg)(r/m) OPCODE(reg,imm): 100000SW 11000(r/m) OPCODE(mem,imm): 100000SW (mod)000(r/m) OPCODE(acc,imm): 0000010W Flag: AF, CF, OF, PF, SF, ZF 의사표현 시작 >>> /* ADD , */ To = To + From; <<< 끝 예제 시작 >>> MOV AX, 0FFFFH ; AX=FFFFH MOV DX, 0000H ; DX=0000H ; DX:AX=FFFF:0000H ADD AX, 0001H ; AX=AX + 0001H ; AX=FFFFH 에 0001H을(를) 더하면 CF=1이 되며 AX는 0000H가 됨 ADC DX, 0000H ; DX=DX + 0000H + CF ; 0000H(DX) + 1(CF) 의 결과를 수행하여 결국 DX:AX=0001H:0000H 가 됨 <<< 끝 활용: 기본 산술연산인 덧셈명령이므로 활용의 범위면에서 굉장히 많습니다. 이 명령은 반드시 숙지해두어야 하며 OPCODE도 외워두는것이 큰 도움이 됩니다. 1.5.7 AND (AND: 논리연산 AND) # 중요도: 매우높음 설명: 논리연산 AND를 수행합니다. OPCODE(reg,reg): 0010000W 11(reg)(r/m) OPCODE(mem,reg): 0010000W (mod)(reg)(r/m) OPCODE(reg,mem): 0010001W (mod)(reg)(r/m) OPCODE(reg,imm): 1000000W 11100(r/m) OPCODE(mem,imm): 1000000W (mod)100(r/m) OPCODE(acc,imm): 0010010W Flag: CF, OF, PF, SF, ZF 의사표현 시작 >>> /* AND , */ To = To & From; <<< 끝 예졔 시작 >>> MOV AX, 0FF00H ; AX=FF00H MOV DX, 5A5AH ; DX=5A5AH (0101101001011010B) AND AX, DX ; AX = AX AND DX ; AX에는 5A00H 가 저장됩니다. <<< 끝 활용: AND연산은 컴퓨터세계에서는 매우 중요한 활용분야가 많습니다. 꼭 OPCODE도 외워두시는것이 좋을듯 합니다. 참고로 CF, OF의 flag변화를 유의하셔야 합니다. 논리연산에서 CF와 OF는 대부분 0으로 clear 하는 특성이 있습니다. 즉, ADD명령과 ADC사이에 AND를 사용하게 되면 CF값이 항상 0이 된다는 점을 꼭 상기해두세요. 1.5.8 CALL (Call a procedure: 구역반환 호출) # 중요도: 매우높음 설명: 함수호출 (다음주소를 Stack push 후에 분기) OPCODE(direct near): 1110100 OPCODE(reg near): 11111111 11010(r/m) OPCODE(mem near): 11111111 (mod)010(r/m) OPCODE(direct far): 10011010 OPCODE(mem far): 11111111 (mod)011(r/m) Flag: 변화없음 의사표현 시작 >>> main() { MyProcedure(); } MyProcedure { AX = 0 } <<< 끝 예제 시작 >>> CALL NEAR PTR MyProcedure ; MyProcedure로 분기 ... MyProcedure: MOV AX, 0 RETN <<< 끝 활용: 수학에서처럼 함수라는 구조를 사용하여 복잡한 작업을 하나의 함수로 정의하고 이를 반복 사용하기 좋은 상태로 만든것을 Procedure라고도 하며 이를 호출하는 명령입니다. 이것은 자신의 CALL명령의 다음주소를 스택에 먼저 PUSH동작을 취한후에 Procedure로 분기하게 되는데 RET명령에 스택에 PUSH된 주소를 POP과 동시에 그곳으로 다시 분기할수 있게 됩니다. 즉, 위의 예제에서 CALL명령 다음 위치의 주소를 스택에 저장후 MyProcedure로 분기하게 되며 AX=0의 동작을 수행하고 RET에 의해서 스택에 저장되었던 주소를 꺼내어 그곳으로 분기하게 됩니다. 이 명령은 구조화에 대한 방법을 제공하는 것이 주요 목적이고 매우 광범위하게 사용됩니다. 1.5.9 CBW (Converrt byte to word: 바이트를 워드로 확장) # 중요도: 보통 설명: AL값을 AX로 확장해줍니다. 만약 AL이 양수이면 AH=00H 이며 음수이면 AH=FFH로 됩니다. OPCODE: 10011000 Flag: 변화없음 의사표현 시작 >>> if( (AL & 0x80) == 0x80 ) { AH = 0xff; } else { AH = 0x00; } <<< 끝 예제 시작 >>> MOV AL, 78H ; AL=78H=(+120D) CBW ; AH=AL값의 부호가 양수이므로 00H, AL=78H MOV AL, 0FFH ; AL=FFH=(-1D) CBW ; AH=AL값의 부호가 음수이므로 FFH, AL=FFH <<< 끝 활용: 보통 나눗셈 명령을 하기전에 이 명령을 사용하여 값을 부호확장하는 경우가 많습니다. 그러나 만약 부호확장이 아닌 AH=00H만을 원한다면 이 명령은 적절치 않습니다. 1.5.10 CLC (Clear carry flag: Carry flag 0으로 초기화) # 중요도: 낮음 설명: CF를 0으로 합니다. 반대로 1로 만드는 명령은 STC가 있습니다. OPCODE: 11111000 Flag: CF 의사표현 시작 >>> CF = 0; <<< 끝 예제 시작 >>> MOV AL, 0FFH ; AL=FFH ADD AL, 01H ; AL=AL + 01H ; CF=1 이 된 상태 CLC ; CF=0 ADC AL, 01H ; AL=AL + 01H + CF(0) <<< 끝 활용: 간혹 정상적인 CF를 무시하고 자신의 루틴이 성공여부를 반환하기 위해서 사용되기도 합니다. 이때 의도적으로 CF를 바꾸기 위해서 사용되는 경우가 있습니다. 1.5.11 CLD (Clear direction flag: Direction flag 를 순방향을 뜻하는 0으로 초기화) # 중요도: 높음 설명: DF를 0으로 합니다. 반대로 1로 하는 명령은 STD가 있습니다. DF=0인경우 순방향이며 DF=1인경우 역방향의 의미를 부여받게 됩니다. OPCODE: 11111100 Flag: DF 의사코드 시작 >>> DF = 0; <<< 끝 예제 시작 >>> MOV AX, 0B800H MOV ES, AX MOV DI, 0000H ; ES:DI=B800H:0000H MOV CX, 100H ; CX=100H MOV AL, 'A' MOV AH, 07H CLD REPZ STOSW ; ES:DI가 가르키는 주소로부터 DI를 증가하면서 100H회 AX를 복사 <<< 끝 활용: 이 명령 혼자만으로는 특별히 하는것이 없지만 스트링명령인 rep와 stos, movs, ins, outs, lods 등이 함께 쓰일때 SI, DI 레지스터값의 증가방향을 결정하는 중요한 DF를 설정합니다. 이 명령은 스트링명령을 정확히 이해해야할 필요가 있습니다. 1.5.12 CLI (Clear interrupt flag: 인터럽트 금지) # 중요도: 매우높음 설명: CF를 0으로 합니다. 반대로 1로 하는 명령은 STI가 있습니다. 이 명령은 인터럽트를 금지하게 됩니다. OPCODE: 11111010 Flag: IF 의사코드 시작 >>> IF = 0; <<< 끝 예제 시작 >>> CLI ; IF=0 MOV AX, CS MOV SS, AX ; SS=CS MOV SP, 0FFFFH ; SP=FFFFH STI ; IF=1 <<< 끝 활용: 하드웨어를 제어하거나 인터럽트 진입시 다른 인터럽트의 발생을 막아야 할 필요가 있는 경우 이 명령을 사용합니다. 시스템 개발자들에게 매우 자주 접하게 되는 명령이며 매우 중요한 명령입니다. 인터럽트 금지시간이 되도록이면 짧게 설계하는 것이 좋습니다. 1.5.13 CMC (Complement carry flag: Carry flag 반전) # 중요도: 낮음 설명: CF를 반전시킵니다. OPCODE: 11110101 Flag: CF 의사명령 시작 >>> CF = !CF; <<< 끝 예제 시작 >>> CLC ; CF=0 CMC ; CF=1 CMC ; CF=0 CMC ; CF=1 ... <<< 끝 활용: CF를 반전하는 경우는 거의 없습니다. 하지만 비효율 구문에서 간혹 회피하기 위해서 사용되기도 합니다. 1.5.14 CMP (Compare: 비교연산) # 중요도: 매우높음 설명: 뺄셈명령인 SUB와 같은 동작을 하지만 실제 값을 변경시키지 않고 Flag만을 변경시킵니다. 결국 비교연산에 활용됩니다. OPCODE(reg,reg): 0011100W 11(reg)(r/m) OPCODE(mem,reg): 0011100W (mod)(reg)(r/m) OPCODE(reg,mem): 0011101W (mod)(reg)(r/m) OPCODE(reg,imm): 100000SW 11111(r/m) OPCODE(mem,imm): 100000SW (mod)111(r/m) OPCODE(acc,imm): 0011110W Flag: AF, CF, OF, PF, SF, ZF 의사명령 시작 >>> /* CMP To, From */ Temp = To - From; <<< 끝 예제 시작 >>> MOV AX, 0; MOV DX, 1; CMP AX, DX ; AX - DX JNE Error ; AX != DX ? jump : keep (ZF == 0 ? jump : keep) ... Error: ... <<< 끝 활용: 두 값을 비교하기 위해서 사용되며 뺄셈과 같은 동작을 하지만 실제 오퍼랜드에 값을 바꾸지 않기 때문에 용이합니다. 1.5.15 CMPS (Compare string: 메모리간의 비교연산) # 중요도: 매우높음 설명: 메모리와 메모리간의 비교 (스트링명령) OPCODE: 1010011W Flag: AF, CF, OF, PF, SF, ZF 의사명령 시작 >>> Temp = ES:[DI] - DS:[SI] <<< 끝 예제 시작 >>> <<< 끝 활용: 1.5.16 CWD (Convert word to double word: 워드를 더블워드로 확장) # 1.5.17 DAA (Decimal adjust for addtion: BCD간의 덧셈결과를 pack BCD형식으로 변환) # 1.5.18 DAS (Decimal adjust for subtraction: BCD간의 뺄셈결과를 pack BCD형식으로 변환) # 1.5.19 DEC (Decrement destination by 1: 단위뺄셈) # 1.5.20 div (Divide: 부호고려없는 나눗셈) # 1.5.21 ESC (Escape: 외부 명령어 전달) # 1.5.22 HLT (Halt: 인터럽트 초기화전까지 정지) # 1.5.23 Idiv (Integer division signed: 부호를 고려한 나눗셈) # 1.5.24 IMUL (Integer multiply: 부호를 고려한 곱셈) # 1.5.25 IN (Input port: 포트로부터 어큐물레이터로 입력) # 1.5.26 INC (Increment destination by 1: 단위덧셈) # 1.5.27 INT (Interrupt: S/W 인터럽트 발생) # 1.5.28 INTO (Interrupt if overflow: Overflow시에 인터럽트 4번 벡터 발생) # 1.5.29 IRET (Interrupt return: 인터럽트로부터 복귀) # 1.5.30 JA (Jump if above: > 이면 분기) # 1.5.31 JAE (Jump if above or equal: >= 이면 분기) # 1.5.32 JNBE (Jump if not below nor equal: <= 이 아니면 분기) # 1.5.33 JNB (Jump if not below: < 이 아니면 분기) # 1.5.34 JB (Jump if below: < 이면 분기) # 1.5.35 JNAE (Jump if not above nor equal: >= 이 아니면 분기) # 1.5.36 JBE # 1.5.37 JNA # 1.5.38 JCXZ # 1.5.39 JE # 1.5.40 JZ # 1.5.41 JG # 1.5.42 JNLE # 1.5.43 JGE # 1.5.44 JNL # 1.5.45 JL # 1.5.46 JGNE # 1.5.47 JLE # 1.5.48 JNG # 1.5.49 JMP # 1.5.50 JNE # 1.5.51 JNZ # 1.5.52 JNO # 1.5.53 JNP # 1.5.54 JPO # 1.5.55 JNS # 1.5.56 JO # 1.5.57 JP # 1.5.58 JPE # 1.5.59 JS # 1.5.60 LAHF (Load AH from 8080 flags: 8080 호환 flags를 AH에 읽어들임) # 1.5.61 LDS (Load DS with register: DS와 레지스터에 읽어들임) # 1.5.62 LEA (Load effective address: 오프셋 주소를 읽어들임) # 1.5.63 LES (Load ES with register: ES와 레지스터에 읽어들임) # 1.5.64 LOCK (Lock bus: Memory bus 일시적 공유제한) # 1.5.65 LODS (Load string: 메모리 읽음) # 1.5.66 LOOP (Loop: 반복분기) # 1.5.67 LOOPZ (Loop if zero: 0인경우와 함께 반복분기) # 1.5.68 LOOPE (Loop if equal: 비교결과 같은경우와 함께 반복분기) # 1.5.69 LOOPNZ (Loop if not zero: 0이 아닌경우와 함께 반복분기) # 1.5.70 LOOPNE (Loop if not equal:비교결과 다른경우와 함께 반복분기) # 1.5.71 MOV (Move: 전송) # 1.5.72 MOVS (Move string: 메모리간 이동) # 1.5.73 MUL (Multiply: 부호를 고려하지 않는 곱셈) # 1.5.74 NEG (Negate: 2의 보수로 변환) #