이전글(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바이트 값을 읽었을 때 같은 값이 나온다.
(1행) ecx 레지스터에 5를 넣는다.
(2~3행) edx 레지스터에 6을 넣는다.
(4행) a에 6을 넣는다.
(5~6행) b에 5+6의 결과인 11을 넣는다.
b는 11이 된다.
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
4. 결론
증감연산자를 한 줄 내에서 여러 개 사용하면 연산의 실행순서가 불분명하여
값이 어떻게 변할지 예측하기 어렵고 컴파일러마다 다른 결과가 나오기도 한다.
따라서 이러한 코드를 작성하지 말아야 할 것이다..
Clang 컴파일러에서는 이런 구문을 컴파일할 경우 'multiple unsequenced modifications' 라는 warning을 띄워 준다.
참고 : undefined behavior