일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 닌텐도 스위치
- 게임보이
- 메트로이드
- ensemble
- 건담
- 티스토리챌린지
- 모빌슈트
- mobilesuit
- 3DS
- 컨트롤러
- 슈퍼마리오
- fpga
- 게임기어
- 패미컴
- 메가드라이브
- 오블완
- 슈퍼패미컴
- PC엔진
- ps4
- mister
- YS
- MSX
- analogue
- 앙상블
- PSP
- snes
- GOG
- 이스
- Apple II
- Game Gear
- Today
- Total
Just a Blog
부끄러운 C 코드 분석 본문
얼마 전 다음과 같은 내용을 보게 되었다.
https://twitter.com/lunasorcery/status/1504893095661424644
C 언어가 JavaScript 같이 동작하지는 않을 것이기 때문에 문자열 "-0.5"가 숫자(numeric) -0.5로 자동 변환되지 않음은 자명한데, 왜 결과가 0.5가 나오는지가 이해가 되지 않았다. 정말 C 언어를 한참 동안 놓고 있어서 감각을 잃어버린 것인가 싶었다.
리눅스에서 코드를 써서 컴파일하고 돌려 봤는데 정말 '0.5'가 출력되었다.
아직도 이해가 안되는 상황이라 결국 컴파일된 어셈블리 언어로 확인해 보기로 했다.
gcc -S test.c -masm=intel
그랬더니 그제서야 상황이 이해되었다.
.file "test.c"
.intel_syntax noprefix
.text
.section .rodata
.LC0:
.string "-0.5"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
lea rax, .LC0[rip+1]
mov rdi, rax
call puts@PLT
nop
pop rbp
.cfi_def_cfa 7, 8
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
결국 해당 계산의 핵심은 이것이었다.
lea rax, .LC0[rip+1]
즉, 텍스트 스트링 "-0.5"가 있는 어드레스에서 +1 만큼 포인터 연산을 한 것이다. 그러니 'puts()' 함수에 전달되는 인자값은 "-0.5" 문자열 시작 어드레스가 아니라 그 어드레스보다 1만큼 증가한 다음 주소, 즉 '-' 캐릭터 위치 다음인 '0' 캐릭터 위치가 된다. 그리고 'puts()'는 그 주소부터 string terminator가 있는 것 까지 출력할 것이니, '-'가 빠진 '0', '.', '5'를 출력하여 우리가 '0.5'를 보게 되는 것이다. 비슷한 식으로 하면 'puts("-0.5"+3)'의 결과는 '5'이다.
코드를 처음 딱 보고 바로 알아차린 사람들도 많을텐데, 바로 이해하지 못한 것이 것이 부끄럽다. 너무 오랫동안 C 언어를 보지 않았나 보다.
<추가>
그 다음에 있는 코드도 혼란스럽게 보인다. 역시 C 언에에서 문자열 "2"가 자동으로 숫자 2로 캐스팅되지 않으며 '**' 같은 산술연산자도 없다.
역어셈블해보면 그냥 50*50을 계산해서 출력한다. 코드의 데이터 섹션에 "%d\n"은 있지만 "2"는 없고 명령어 오퍼랜드로 50이 두 번 사용된다. 큰 힌트는, 캐릭터 '2'의 ASCII 코드 값이 50이라는 것!
결국, 두 경우 모두 C 언어에서 하나의 중요한 특징을 강조하는 듯 하다. "C 코드에서 문자열 표현 구문은 결국 '주소(address)'다."