이전글(2013년) : http://tibyte.kr/160



단항 연산자(unary operator) 중에서 전위 증가(pre-increment)연산자와 후위 증가(post-increment) 연산자를 같은 줄에 사용한 결과를 3개의 컴파일러로 컴파일해 보았다.

이는 undefined behavior로, 컴파일러마다 다른 결과가 나올 수 있다.


사용한 컴파일러는 gcc 4.8.4와 clang 3.4, 그리고 Visual Studio 2015에 포함된 C 컴파일러(이하 VC)이다.



a의 초기값을 5로 했을 때 결과는 아래와 같다.

(원본 코드는 https://github.com/tibyte/asm/blob/master/unary.c 에서 볼 수 있다.)


위 그림에서처럼 컴파일러에 따라 결과가 다르게 나오는 것을 볼 수 있다.


소스코드가 어떻게 번역됐는지 확인하기 위해 오브젝트 파일을 생성하고 디스어셈블한다.






1. 디스어셈블된 코드 생성




 

$gcc -g -c unary.c -o unary-gcc.o

$objdump -dS -M intel unary-gcc.o > unary-gcc.txt

$clang -g -c unary.c -o unary-clang.o

$objdump -dS -M intel unary-clang.o > unary-clang.txt



gcc와 clang에서 -c옵션은 오브젝트 파일을 생성하는 것이고 -g는 디버깅 옵션으로, 소스코드를 포함시킬 수 있다.


objdump에서 -d옵션은 디스어셈블,  -S옵션은 소스코드 포함 옵션이다.

-M옵션은 인스트럭션을 AT&T 문법으로 출력할 것인지, Intel문법으로 출력할 것인지를 결정하는 옵션인데, 기본값은 AT&T로, 아래와 같은 형태로 출력된다.  





-M intel 옵션을 붙여 주면 아래와 같이 인텔 형식으로 된 결과를 얻을 수 있다.






Microsoft Visual Studio 2015에서 디스어셈블된 코드를 확인하려면

코드에 중단점을 건 후 F5로 디버그를 시작하고  Debug - Windows - Disassembly를 클릭하면 된다.







2. b = (a++) + a 분석


a의 초기값은 5이다.


GCC

mov    eax,DWORD PTR [a]

lea    edx,[rax+0x1]

mov    DWORD PTR [a],edx

mov    edx,DWORD PTR [a]

add    eax,edx

mov    DWORD PTR [b],eax

(1) eax 레지스터에 5를 넣는다.

(2) edx 레지스터에 6을 넣는다.

(3) a에 6을 넣는다.

(4) edx 레지스터에 6을 넣는다.

(5~6행) b에 5+6의 결과인 11을 넣는다.

b는 11이 된다.


lea 명령에서 rax 레지스터는 eax레지스터의 64비트 버전이므로

저장된 4바이트 값을 읽었을 때 같은 값이 나온다.



Clang
mov    ecx,DWORD PTR [a]
mov    edx,ecx
add    edx,0x1
mov    DWORD PTR [a],edx
add    ecx,DWORD PTR [a]
mov    DWORD PTR [b],ecx

(1행) ecx 레지스터에 5를 넣는다.

(2~3행) edx 레지스터에 6을 넣는다.

(4행) a에 6을 넣는다.

(5~6행) b에 5+6의 결과인 11을 넣는다.

b는 11이 된다.


VC
mov         eax,dword ptr [a] 
add         eax,dword ptr [a] 
mov         dword ptr [b],eax 
mov         ecx,dword ptr [a] 
add         ecx,1  
mov         dword ptr [a],ecx 
(1~2행) eax 레지스터에 6을 넣는다.
(3행) b에 6을 넣는다.
(4~6행) a에 5+1의 결과인 6을 넣는다.

b는 10이 된다.






3. b = (++a) + (a++) 분석


a의 초기값은 5이다.


GCC

add    DWORD PTR [a],0x1

mov    eax,DWORD PTR [a]

lea    edx,[rax+0x1]

mov    DWORD PTR [a],edx

mov    edx,DWORD PTR [a]

add    eax,edx

mov    DWORD PTR [b],eax

b에는 eax값과 edx값의 합이 들어가는데, 맨 처음에 a에 1을 더하여 6이 되고
그 다음 복사해 온 eax값에 1을 또 더하여 edx 레지스터에 복사하므로
b는 6+7의 결과인 13이 된다.


Clang
mov    ecx,DWORD PTR [a]
add    ecx,0x1
mov    DWORD PTR [a],ecx
mov    edx,DWORD PTR [a]
mov    esi,edx
add    esi,0x1
mov    DWORD PTR [a],esi
add    ecx,edx
mov    DWORD PTR [b],ecx
gcc와는 다르게 esi 레지스터를 사용하여 더한 값은 ecx+edx의 연산결과에 포함되지 않으므로
b의 값은 6+6의 결과인 12가 된다.



VC
 mov         eax,dword ptr [a] 
 add         eax,1  
 mov         dword ptr [a],eax 
 mov         ecx,dword ptr [a] 
 add         ecx,dword ptr [a] 
 mov         dword ptr [b],ecx 
 mov         edx,dword ptr [a] 
 add         edx,1  
 mov         dword ptr [a],edx 
clang 결과와 비슷하게 a에 대한 증가 연산 중 하나가 b값이 결정된 후에 계산되므로
b는 6+6 = 12가 된다.




4. 결론



증감연산자를 한 줄 내에서 여러 개 사용하면 연산의 실행순서가 불분명하여

값이 어떻게 변할지 예측하기 어렵고 컴파일러마다 다른 결과가 나오기도 한다.

따라서 이러한 코드를 작성하지 말아야 할 것이다..


Clang 컴파일러에서는 이런 구문을 컴파일할 경우 'multiple unsequenced modifications' 라는 warning을 띄워 준다.





참고 : undefined behavior



+ Recent posts