[asm] 0을 만드는 방법

어셈블리 코드를 보면 유난히 많이 나오는 코드가 다음과 같은 코드이다.
xor eax, eax

무슨 뜻인가 싶어서 구글링 해보니 레지스터를 0으로 만드는 명령이란다. 그냥 mov eax,0 으로 하면 될 것을 왜 그리 복잡하게 생각할까 해서 다시 구글링 해보니 명령어의 크기가 다르다고 한다. 정말인가 싶어서 디스어셈블을 해봤다.

(1) 31 C0                            XOR EAX,EAX
 
(2) 29 C0                            SUB EAX,EAX
 
(3) 83 E0 00                        AND EAX,0
 
(4) C1 E8 1F                       SHR EAX,$1F
 
(5) B8 00 00 00 00                MOV EAX,$00000000


xor eax, eax 와 sub eax, eax 는 각각 2바이트로 명령어의 크기가 가장 작다. 같은 크기면 sub eax, eax 를 쓸 수도 있을 것 같은데 사람들이 그건 별로 좋아하지 않는 모양이다. 그럼 궁금해지는 것이 어떻게 0으로 만드는가이다. 일단 eax 에 0011E1F3 이 있다고 가정한다.

2진수로 바꾸고 한바이트씩 끊어서 표시하면 00000000 00010001 11100001 11110011

(1) xor eax, eax
xor 연산은 or 연산과 비슷하지만 둘다 1일 경우 0으로 만드는 점이 다르다. 따라서 둘다 0일 경우는 or 연산과 비슷하게 0이 된다. xor 는 둘다 1일 경우도 0이므로


입력 (INPUT) 출력 (OUTPUT)
A B A XOR B
0 0 0
0 1 1
1 0 1
1 1 0

00000000 00010001 11100001 11110011
00000000 00010001 11100001 11110011 xor
00000000 00000000 00000000 00000000

(2) sub eax, eax
sub 연산은 첫번째 오퍼랜드에서 두번째 오퍼랜드를 빼므로 자기가 자기를 빼는 형태이므로 당연히 0이다. 결론은 당연히 (1)과 같다.

(3) and eax, 0
and 연산은 둘 다 1일경우만 1이고 나머지는 모두 0이므로. 0과의 and는 모두 0이다.

00000000 00010001 11100001 11110011
00000000 00000000 00000000 00000000 and
00000000 00000000 00000000 00000000

논리적으로는 (1), (2), (3) 모두 같은 결과를 보여주지만 명령어의 크기가 다른 것은 왜일까. 이럴땐 모양을 잘 봐야 한다. (1), (2) 의 모양을 자세히 관찰해 보자.

31 C0                            XOR EAX,EAX
 
29 C0                            SUB EAX,EAX

두번째 바이트가 모두 C0 이다. 그리고 첫번째, 두번째 오퍼랜드도 eax 로 같다. eax, eax 가 C0 로 표현된 것을 아닐까? 자세히 알아보기 인텔의 매뉴얼을 보자. 일단 xor 명령어를 보자. 인텔의 매뉴얼에서 xor 은 31/r 로 표현된다.

사용자 삽입 이미지

그 다음 32-Bit Addressing Forms with the Mod R/M Byte 도표를 보자. 가로줄은 첫번째 오퍼랜드, 세로줄은 두번째 오퍼랜드이다. 첫번째와 두번째 모두 eax 이므로 C0 이다. 만약, xor ecx, eax 와 같이 한다면  31 C1 이 된다.

사용자 삽입 이미지

sub 는 29/r 로 표현된다. 첫번째 , 두번째 오퍼랜드가 모두 eax 이므로 마찬가지로 C0 이다.

사용자 삽입 이미지

(3)의 모양을 보자.

83 E0 00                        AND EAX,0

일단 and 를 찾는다. 여러개가 있지만 83을 찾는다.

사용자 삽입 이미지

83/4  라고 나와있는데 이것은 4를 기준으로 해서 목적지 오퍼랜드를 찾으라는 것이다. 4를 기준으로(세로줄) eax 를 가로줄에서 찾으면 E0 가 된다.
사용자 삽입 이미지

그리고 and 시킬 값인 0은 1바이트 이므로 모두 3바이트가 된다.

(4)는 오른쪽으로 이동(shift right) 을 31개 하면 0으로 채워진다. shr 은 C1/ 5 이다.

사용자 삽입 이미지
 
(4) 는 (3) 과 같은 방법으로 찾는다. shr 은 C1/ 5 이므로 5를 기준으로 목적지 오퍼랜드가 eax 인 것을 찾는다.
사용자 삽입 이미지

(5) 는 약간 특이하다. mov 와 eax 가 따로 있는 것이 아니라 mov eax 가 하나의 명령어로 되어 있다. mov eax 는 B8 이다.
사용자 삽입 이미지

레지스터의 순서에 따라서 번호가 증가한다. eax  다음은 ecx edx 순으로 각각 B9, BA 가 된다. 명령자체는 단순하지만 움직이는 데이터가 4바이트나 되므로 전체적인 명령의 크기가 커져 버리고 만다. 요즘 컴퓨터를 기준으로 했을 때는 xor eax, eax 와 mov eax, 0 의 성능차이가 없다고 하지만 xor eax, eax 를 사용하면 프로그램의 크기가 작아지는 효과는 있다.

직관적으로 이해하기 쉽다는 것은 사람의 입장에서 그렇다는 것이고 기계의 입장에서는 어짜피 bit 단위로 이해하므로 bit 단위의 해석이 더 '기계친화적'일 수 있겠다. 어짜피 어셈블리까지 내려갈 정도면 bit 단위 해석도 익숙하다는 이야기가 될 것이고 xor eax, eax 가 mov eax, 0 보다 더 어렵게 느껴지지는 않을 테니 말이다.


P.S. 인텔 매뉴얼을 정독하지 않고 눈치로 때려 맞춘 것이므로 정확하지 않을수도 있음. 매뉴얼 보기 너무 짜증나는데, 보기 편하게 정리해 놓은 곳도 없으니 뭐...