기타 케이블이 톤에 영향을 미친다는 말은 많이 들어봤을 것입니다.
이것이 단순히 심리적인 이유 때문인지, 정말로 기타 톤에 영향을 미치는지 평소에 궁금해 하던 차에
기회가 닿게 되어 한번 정리를 해보았습니다.

- 기타 케이블의 가장 중요한 포인트는 케이블 자체 커패시턴스이다.
- 일반적인 카나레/모가미 등등의 케이블 자체 커패시턴스는 대략 100~150pF (m당 3~4천원)
- 고급 케이블 커패시턴스는 50pF 미만이나 상당히 고가이다.
케이블 자체 커패시턴스는 고역을 깍아먹는다. (로우패스/하이컷)

몇몇 잘 알려진 기타 케이블의 m당 커패시턴스는 다음과 같습니다.

- 카나레 L-2T2S (23AWG, 2심): 심-심: 70pF/m, 심-실드: 106pF/m (합산 ~176pF/m, 실제 측정값 ~121pF/m)
- 벨덴 1800F (24AWG, 2심): 심-심: 39pF, 심-실드: 85pF/m (합산 ~124pF/m), 벌크선재 ~4.5천원/m
- 모가미 2552/2582(20AWG, 2심): 심-심: 10pF/m, 심-실드: 90pF/m (합산 ~100pF/m), 벌크선재 ~2천원/m
- 클로츠 MY206SW,MC2000SW(24AWG): 심-심:60pF/m, 심-실드: 110pF/m (합산 ~170pF), 벌크선재 ~MY206: 3천원/m
모가미 2549 (22AWG, 2심): 심-심: 11pF/m, 심-실드: 76pF/m (합산 ~87pF/m , 실측 ~87pF/m)
- 벨덴 8412 (20AWG,2심): 심-심: 110pF/m, 심-실드: 190pF/m (합산 ~300pF/m), 벌크선재 ~8천원/m, 빈티지

- 카나레 GS-6(18AWG): 심-실드: 160pF/m
- 벨덴 9394(20AWG)/9395(18AWG): 심-실드 180pF/m
- 벨덴 9778(20AWG): 심-실드 148pF/m
- 벨덴 8410(25AWG): 심-실드: 108pF/m
- 클로츠 AC104SW(24AWG): 심-실드: 115pF/m
- 모가미 2319(23AWG): 심-실드 155pF/m
- 모가미 2524(20AWG): 심-실드 130pF/m

- 반담 Pro Grade Classic XKE instrument: 심-실드 90pF/m (벌크선재  ~3천원/m, 시중에서 판매중)
- 좀머 Sprit XXL(18AWG): 심-실드 86pF/m (벌크 선재 5~6천원/m, 시중에 판매중)
- 코디알 CGK 122: 심-실드 82pF/m (시중에서 완제품만 판매중)
- 좀머 Classique(20AWG): 심-실드 78pF/m (벌크 선재 ~6천원/m, 시중에 판매중)
- 갭코 XB20UB: 심-실드 73pF/m (벌크 선재 ~5천원/m, 시중에 판매중)
- 모가미 3368(20AWG): 심-실드 70pF/m (상당히 두꺼움, 벌크 선재 ~1만3천원/m, 시중에 판매중)
- 고담 GAC-1 Ultra Pro(24AWG): 심-실드 70pF/m (벌크 선재 ~1만원/m, 시중에 판매중)
- 호사 CGK(20AWG): 심-실드 68pF/m (시중에서 완제품만 판매중)
- 클로츠 AC110(24AWG): 심-실드 65pF/m (완제품만 판매중. 5m 최저가 4만원대(+뉴트릭 NP2X-B 2개))
- 좀머 Spirit LLX: 심-실드 52pF/m (벌크 선재 ~1만원/m, ebay에서 판매중)

다음 링크는 이를 잘 정리해놓은 케이블 커패시턴스 테이블입니다.
https://www.shootoutguitarcables.com/guitar-cables-explained/capacitance-chart.html

 

Guitar Cable Capacitance Chart • Comparison of pF Ratings by SHOOTOUT! Guitar Cables UK

GUITAR CABLE CAPACITANCE CHART From: Shootout Guitar Cables UK • Best Guitar Cables Explained Guitar Cable Capacitance and Resonant Frequency See also: The Shootout Guitar Cables UK Range From our knowledge of Guitar Cable Capacitance and Resonant Freque

www.shootoutguitarcables.com

케이블 커패시턴스는 고역을 깍아먹는다

케이블은 자체적인 커패시턴스는 고역을 깍이게 만든다고 하는데 이러한 원리는 무었일까요? 전자공학에 약간만 관심을 가진 분들이라면 하이패스/로우패스 필터 회로등을 아실 것입니다. 케이블이 가지고 있는 자체 커패시턴스는 다음과 같은 등가회로로 표현되는데 이는 하이컷 혹은 로우패스 필터 회로를 구성하게 된다는 것을 뜻합니다.

이 필터회로는 시그널의 진동수가 낮은 경우에는 커패시턴스를 통해 신호가 거의 유실되지 않고 통과하게 되나, 어느 진동수 이상의 시그널이 전달되는 경우에는 C1 커패시턴스를 통해 신호가 유실되게 된다는 것입니다. 신호가 유실되는 시점의 진동수를 차단 진동수라고 부르며 차단진동수 fc값은 1/(2π × R × C) 입니다.

예를 들어, 케이블의 저항값이 대략 10오옴이라 하고, 커패시턴스 값을 500pF이라고 한다면, 차단 주파수는 1/(2*3.141592*10*500e-12) = 31.8MHz 입니다. 그렇다면 이 경우에는 차단 주파수가 가청주파수 20~20kHz에서 한참을 벗어나게 되므로, 하이컷 필터가 작동하지 않는 게 맞을 것으로 생각됩니다만, 실제로는 단순히 위와 같은 회로로 작동하는 것이 아닌 하이컷 필터가 기타 픽업회로의 일부로 작동하기 때문에, 회로 앞단에 기타 픽업과 볼륨 노브(저항값)가 물려있는 것으로 본다면, 저항값은 픽업의 5k오옴과 볼륨 노브값에 해당하는 대략 200k오옴의 합산이 될 것이며, 이 경우 차단 파수값을 공식에 집어넣어보면 1/(2*3.141592*205*1000*500e-12) = 1.55kHz 근방에 위치하게 됩니다. 즉, 기타 케이블 커패시턴스가 500pF이라면 차단주파수가 1.5kHz 근처에서 하이컷이 걸려 기타의 고역이 깍이게 됩니다.

저렴한 기타 케이블 3m의 커패시턴스가 대략 500pF정도 되기때문에 1.5kHz 근처의 고역이 날아가버리게 된다는 것인데, 커패시턴스가 200pF이 되는 좀 더 좋은 케이블을 쓰게 되면 차단 주파수는 ~3.88kHz (=1/(2*3.1415926535*205*1000*200e-12))가 되어 고역이 덜 깍이게 될 것입니다.

LTSpice를 이용한 기타 회로 모델링 및 테스트

단순히 로우패스 필터의 차단주파수만으로 케이블 커패시턴스를 설명할 것이 아니라 회로 시뮬레이션 프로그램인 LTSpice를 통해서 기타 픽업회로를 구성하여 이를 테스트해볼 수도 있을 것입니다.
구글에서 검색을 해보니 누군가가 이미 2016년도에 해놓았더군요.
https://guitarnuts2.proboards.com/thread/7842/modeling-electric-guitar-ltspice

 

Modeling an electric guitar with LTSpice | GuitarNutz 2

Here in the future, electrical circuits can be simulated with software before anyone ever has to worry about the practical matter of how they will be made. As electric guitars are literally

guitarnuts2.proboards.com

저도 이를 따라 LTSpice를 통해 직접 픽업회로를 구성해보았습니다. (asc 파일 다운로드는 다음 링크 참조)

guitar-cable-capacitance.zip
0.00MB

- 패시브 픽업 회로의 경우 (액티브 픽업회로라면 케이블 커패시턴스에 의한 영향은 거의 없다고 봐도 무방하다)
- 기타 픽업의 헨리값은 대략 1.8~3.2H
- 픽업 자체 저항값은 5k~10k오옴
- 픽업 커패시턴스는 50~300pF
- 케이블의 커패시턴스값은 50p~1000p 정도.

볼륨이 높을 때

위 그래프는 볼륨이 거의 최대인 경우, .step param cc list 1000p 500p 200p 100p 50p 명령을 통해서 각각 1nF, 500pF, 200pF, 100pF, 50pF 그래프를 한꺼번에 그리고, .ac oct 100 20 50000 실행 명령을 통해 20hz~50kHz 영역을 100mV 교류 시그널로 sweep한 것입니다.

- 커패시턴스가 1000pF정도 되는 경우 4kHz 근방부터 깍이며, 약 5dB정도의 부스트가 되는 것도 볼 수 있다.
- 커패시턴스가 200pF정도 낮아지면 7kHz 근처에서 꺽이고 약 2dB정도의 부스트가 된다.

볼륨이 중간정도일 때

위 그래프는 볼륨을 어느정도 낮추었을 때에 그래프입니다. (볼륨이 높을때의 그래프보다 약 6dB정도 낮아졌습니다)

이 경우에는 커패시턴스가 높으면 2kHz 근방부터 천천히 깍이게 되며, 커패시턴스가 100pF정도로 낮으면 10kHz 정도에 살짝 부스트가 있으면서 천천히 감쇠하는 것을 볼 수 있습니다. 케이블 커패시턴스가 대략 200pF정도만 되어도 상당히 양호한 결과를 얻는 것을 볼 수 있습니다.

또한, 케이블 커패시턴스를 200pF으로 고정하고, 볼륨 노브를 조정한 경우에는 다음과 같은 그래프를 얻습니다.

즉, 볼륨 높이면 차단 주파수가 낮아지고 고음이 더 깍이고 약간 부스팅되며, 볼륨을 낮추면 차단주파수가 높아지며 부스팅도 줄어들고 깍이는 고음영역이 좁아지게 됩니다.

(위에서 소개한 원본 링크에는 실제 상황에서 픽업에 의해 유도된 시그널의 전압이 일정하지 않는 다는 점을 지적하고 있으며 싱글픽업 뿐만 아니라 험버커 픽업에 대한 시뮬레이션도 포함하고 있으니 같이 참조하시기 바랍니다.)

그밖에 ltspice로 몇가지 테스트를 더 해보면
- pickup의 henry수가 5H 이상인 경우 컷 주파수가 더 많이 내려오게 되고, 케이블 커패시턴스의 영향이 줄어든다.
- pickup의 커패시턴스가 50p이상으로 높아지면 컷 주파수가 더 내려온다. 픽업의 커패시턴스가 낮을 수록 좋다.

케이블 이외에 커패시턴스에 영향을 미치는 요소는?

https://guitarnuts2.proboards.com/thread/7725/capacitive-coupling-various-guitar-parts 링크를 보면 여러 요소들이 기타 케이블 커패시턴스에 영향을 미치는 것을 볼 수 있습니다.

이에 의하면 55커넥터는 물론 기타에 부착된 플러그까지 수 pF정도의 커패시턴스를 가지며, 기타 픽업회로의 케이블 커패시턴스에 더해지게 됩니다. 55커넥터의 커패시턴스를 LCR미터로 측정해보면 ~20pF 근처가 나오므로, 케이블 커패시턴스 + 2 * 55커넥터 커패시턴스 +  = 전체 케이블 커패시턴스가 됩니다.

https://www.vertexeffects.com/blog/vertex_choosing_right_connector 페이지에서는 여러 플러그들의 커패시턴스에 대한 측정값이 정리되어 있습니다.

2심선의 커패시턴스

2심 발란스선은 커팅해서 판매하는 곳이 많고 상당히 저렴한 편입니다. 예를 들어 카나레 L-2T2S 경우는 상당히 저렴한 반면 카나레 GS-6는 이보다 좀 더 비싼데, L-2T2S의 커패시턴스와 거의 같거나 낫습니다.

실제로 LCR미터로 커패시턴스를 측정해보면 GS-6의 경우에는 약 150pF/m가 나오며, L-2T2S의 경우 심-실드값이 121pF/m이 나왔습니다. (L-2T2S의 경우 스펙보다 약간 높게, GS-6는 약간 낮게 나옴. 참고로 L-2T2S의 심-심 커패시턴스는 ~66pF/m 정도로 스펙보다 조금 높게 나옴)

간혹 일부 블로그에서 2심선의 Cold와 Hot선을 묶어서 만드는 경우가 있는데, 이렇게 2심선을 묶어버리면 거의 두배의 커패시턴스가 나옵니다. L-2T2S의 경우 Cold와 Hot선을 묶으면 LCR미터로 224pF/m가 나왔습니다.

L-2T2S 2심 발란스선을 이용하에 다음과 같이 만드는 경우 각각의 커패시턴스가 약간의 차이만 납니다. (예상할 수 있듯이 Cold선과 실드를 묶은 케이블의 커패시턴스가 약간 높음)

한쪽은 실드+Cold선을 묶은 경우. 총 커패시턴스 = 심-심 + 심-실드?
Cold선을 아예 쓰지 않는 경우 총 커패시턴스 = 심-실드 ?

L-2T2S의 경우 실드선와 Cold선을 묶어 그라운드로 하면 심-심 70pF/m + 심-실드 106pF = 176pF/m  정도로 예상할 수 있는데, Cold선을 아예 사용하지 않는 경우도 심-실드 커패시턴스에 해당하는 106pF이 나오는 것이 아니라, 심-심+심-실드 합산한 값에 가까운 값이 나오게 됩니다. (실제, LCR 미터로 간략히 측정해보면 심-심= 66pF/m, 심-실드=~121pF/m정도 나오지만, Cold선과 실드를 묶는 경우에도 ~121pF/m 혹은 이보다 약간 높게 나옴)

가성비 좋은 모가미 2549

L-2T2S와 가격대가 거의 같은 모가미 2549의 경우에도 Cold선 실드를 묶는 경우와 그렇지 않은 경우에 대해 LCR미터로 측정해보니 각각 87pF/m(심-실드 측정), ~89pF/m(Cold심+실드 묶어 측정) 정도 측정되어 스펙상 심-실드 + 심-심 값에 해당하는 87pF/m와 거의 같았습니다.

100pF/m 이하의 인터넷 쇼핑몰상에서 그나마 구하기 쉬운 단심 언발란스선은 좀머 SC-Spirit XXL인데, 모가미 2549보다 두배의 가격입니다. 좀머 XXL의 커패시턴스를 LCR미터로 측정해보면 스펙과 거의 같으나 모가미 2549와 거의 비슷한 ~87pF/m가 나옵니다. 이는 그 절반 가격인 모가미 2549와 커패시턴스 특성으로는 거의 동급이라는 것을 뜻합니다.

(몇몇 케이블을 직접 구입해서 일반적인 멀티미터로 간단히 측정해보면 대부분 스펙상의 커패시턴스보다 20~40pF/m정도 높게 측정되었습니다. 따라서 좀 더 정확한 측정을 위해서는 LCR 미터를 사용해야만 하였습니다.)

또한, 모가미 2549의 경우 심-심 LCR미터 측정 커패시턴스는 51pF/m가 나오는데, 이는 스펙상 11pF/m에 많이 차이가 나는 값이며, 아마도 심-심 측정의 경우 이를 감싸고 있는 실드에 의한 영향을 고려하여 차감해준 것으로 생각됩니다. (실험삼아 노출된 두 전선을 10cm정도의 알루미늄 호일로 감싸기만 해도 ~3pF정도 값이 상승. 이의 10배인 1m에 해당하는 3*10 = 30pF을 51pF/m에 차감해주면 21pF/m이 되어 스펙상 수치와 얼추 비슷해짐)

나에게 적당한 케이블은?

여기서 살펴볼 수 있듯이, 기타줄의 커패시턴스 증감은 3k~5k 중음역, 중고음역 대역을 건드리게 됩니다. 이 그래프대로라면, 대략 200pF 이하의 저용량 커패시턴스를 가지는 케이블은 3k~5k 대역의 중고음역을 덜 깍아먹게 하여 중고음역의 손실을 최소화 하게 됩니다. 일렉기타 + 믹서 혹은 기타 후처리를 하실 분들이라면 악기에서 나오는 음역을 최대한 손실 없이 받은 후에 믹서등을 사용하여 EQ적용을 하시려면, 커패시턴스가 200pF~300pF 정도의 케이블이 적합할 것입니다.

반면, 3k 이상의 중고음역의 소리를 불편하게 여기는 분들이시라면 오히려 500pF 커패시턴스 이상의 케이블을 선호할 수도 있습니다. 고용량 커패시턴스의 케이블은 중고음역대에 손실을 일으키고 이는 EQ 역할을 하게 되는 것입니다. 시중에서 빈티지 케이블이라 불리는 케이블이 바로 이러한 경우더군요. 미터당 1.8만원이나 하는 고가의 빈티지 모 커스텀 케이블 3m짜리가 LCR미터로 커패시턴스를 측정해보니 550pF이 나왔습니다. 55짹의 커패시턴스가 대략 15~20pF정도 나오니  선재 1m당 170~173pF 커패시턴스라는 것인데, 이 수치는 카나레 GS-6와 비슷합니다.

따라서 위의 두가지 경우에 따라 자신이 선택해야 하는 선재를 고르면 될 것입니다. 

케이블 길이에 따라 커패시턴스가 좌우된다

고가의 100pF/m 커패시턴스 이하 선재의 경우 2m정도 길이의 케이블을 만든다면 저 용량 커패시턴스를 가지는 케이블을 만들 수 있습니다. 미터당 50pF의 고급 선재를 가지고 있다 하더라도 10m 이상의 길이로 만들면 케이블 전체의 커패시턴스가 500pF이 되어서 원래 의도한 저용량 커패시턴스와는 멀어질 수 있다는 것을 뜻하기도 합니다.

또한 500pF 이상의 고용량의 커패시턴스가 필요하다면 케이블의 길이를 늘리면 됩니다. 120pF정도의 커패시턴스를 가지는 선재가 있고, 600pF 커패시턴스의 용량을 가지는 케이블을 만든다면,  약 5m길이의 케이블을 만들면 630~640pF정도의 커패시턴스를 가지는 선재가 됩니다.

(그리고 위에서 설명했던 두번째 그래프에서 보았듯이, 볼륨을 너무 높이거나 낮추지 말고 적정 수준으로 유지해야만 부스팅 되면서 꺽여지는 주파수 영역대를 어느정도 보완할 수 있다는 것도 함께 고려해야 합니다)

- 모가미 2549는 심-심 + 심-쉴드 합산하여 87pF정도 되는데 이는 상당히 저렴하고 쓸만한 케이블을 만들 수 있다는 뜻
- 모가미 3368로 5m선재를 만들면 350pF이 된다. 반면 모가미 2549로 3m 선재를 만들면 261pF이 되어 오히려 더 낫다. 고가의 모가미 3368을 쓰기보다는 좀 더 저렴한 선재로 짧은 길이로 만드는 것이 나을 것이다.
- 베이스 기타의 경우에는 별 신경을 쓰지 않더라도 괜찮다고 생각할 수도 있으나, 케이블 커패시턴스가 1000pF 이상이 된다면 800Hz~영역 근처부터 손실되는 것을 예상할수 있으므로 이는 피해야 할 것이다.

LCR미터

본문에서 측정에 사용된 LCR미터는 인터넷쇼핑몰상에서 가장 저렴한 축에 속하는 루트론 LCR-9063을 사용했습니다. 설명서를 보면 2nF 아래의 측정에 사용된 주파수는 250Hz입니다. REL 기능이 없기때문에, open된 상태에서 측정된 ~9~12pF정도의 수치를 빼주어야 측정값이 됩니다. 

문서 변경사항

- 7/12 - 최초 문서 작성
- 7/14 - LCR미터를 이용한 측정으로 문서 수정, 모가미 2549 섹션 추가
- 7/16 - 차단진동수 계산식수정

by dumpcookie 2022. 7. 11. 22:08

지난번에는 간단한 C언어 예제를 objconv 및 nasm/yasm을 통해서 x86 어셈블리어로 변환하는 방식을 살펴본 바 있다. objconv 프로그램은 상당히 완성도가 높았으며, MASM 혹은 인텔 어셈블리어 문법에 익숙한 사용자에게 쓸만한 고급언어 -> x86 어셈블리어 변환 용도로 활용될 수 있을 것이고, 어셈블리어를 최적화하는 용도 혹은 기타 어셈블리어를 배우는 등의 활용을 할 수 있다는 점을 살펴 보았다. 이번에는 인라인 어셈블리어에 대해 살펴보려고 한다.

상당히 많은 수의 유명 라이브러리는 고급 언어로 작성되어있지만, 게중에 일부는 특정 CPU에 더욱 최적화되어 있는데, 최적화된 부분은 보통 어셈블리어를 통해 이루어지고 있다. 프로그램 소스 전체가 모두 어셈블리어로 작성된 경우는 거의 찾아보기 어렵고 보통은 해당 라이브러리의 일정 부분이 어셈블리어를 통해 최적화 된 형태이다. 또한 어셈블리어가 별도의 소스로 완전히 분리되어 있는 경우도 있지만, C언어(혹은 다른 고급 언어)와 어셈블리어를 섞어서 사용하는 경우가 상당수인데, C 소스에 어셈블리어를 섞어서 사용하는 기법을 인라인 어셈블리(Inline Assembly)라고 부른다.

gcc는 내장된 GAS(GNU Assembler) 어셈블러를 가지며, 이것이 기본으로 지원하는 어셈블리어는 다음과 같은 AT&T 스타일의 어셈블리어이다.

mov $0x05, %eax # 주석

이것은 16진수 0x5값을 eax 레지스터에 넣으라는 명령이다. 이 명령에 대응하는 인텔 어셈블리어 스타일은 다음과 같다.

mov eax, 0x05

인텔 스타일과 AT&T 스타일을 비교해보면, 이 경우에는 명령코드의 소스와 대상의 위치가 바뀌고, 레지스터를 구분하는 prefix가 %, 숫자를 구분시켜주는 prefix로 $를 붙이는 것이 다를 뿐 거의 다를바가 없다. 최신 gcc의 경우에는 인텔 스타일 및 AT&T 스타일 모두 사용하는 것이 가능하나, 전통적으로(?) AT&T 스타일로 인라인 에셈블리어를 사용한다. 이미 x86 어셈블리어에 익숙한 사용자라면 조금 헷갈릴 수는 있으나, 이하 예제에서 나올 인라인 에셈블리 스타일은 모두 AT&T 스타일임에 유의하자.)

첫번째 예제

이제 다음의 아주 간단한 인라인 어셈블러 예제를 살펴보자.

int main() {
        int dst = 0;
        __asm__ volatile (
                "mov $100, %%eax\n\t"
                "add $10, %%eax\n\t"
                "mov %%eax, %0\n\t"
                : "=m" (dst)
        );
        return dst;
}

소스를 보면 대충 예측할 수 있을 정도로 간단하지만, 이 간단한 인라인 어셈블리 프로그램이 하는 것은 다음과 같다.

1) 숫자 100을 %eax 레지스터에 넣고, 2) 숫자 10을 %eax 레지스터에 더하여 얻어진 것을 0번째 출력 인자 dst에 넣는다. 그러므로 dst값은 110이 될 것이고 main() 함수는 110을 리턴하게 된다. 위의 경우 어셈블리어를 세줄로 나누어 보기 좋게 하였으나, 다음과 같이 한줄로 넣는 것도 물론 가능하다.

"mov $100, %%eax\n\tadd $10, %%eax\n\tmov %%eax, %0\n\t"

그러나 이렇게 소스를 만들면 조금 보기 불편하므로 여러줄로 보통 나누어 쓴다. 또한 각 줄은 "\n\t" 문자 즉, 줄넘김(\n) 문자를 필수적으로 넣어주어야 하고, 여기에 탭(\t) 문자를 추가적으로 넣어주어서 gcc -S 옵션으로 컴파일 할 경우 얻을 수 있는 어셈블리어 소스에 들여쓰기가 되도록 해준다.

위 소스코드 예제를 test.c라는 이름으로 저장하고 이를 다음과 같은 명령으로 컴파일을 해서 어셈블러 소스코드 test.s를 얻어보면 다음과 같다. (실행 환경 우분투 16.04.4 / gcc 버전 5.4.0)
$ gcc -O2 -m32 -S test.c

        .file   "test.c"
        .section        .text.unlikely,"ax",@progbits
.LCOLDB0:
        .section        .text.startup,"ax",@progbits
.LHOTB0:
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        leal    4(%esp), %ecx
        .cfi_def_cfa 1, 0
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        .cfi_escape 0x10,0x5,0x2,0x75,0
        movl    %esp, %ebp
        pushl   %ecx
        .cfi_escape 0xf,0x3,0x75,0x7c,0x6
        subl    $20, %esp
        movl    %gs:20, %eax
        movl    %eax, -12(%ebp)
        xorl    %eax, %eax
#APP
# 3 "test.c" 1
        mov $100, %eax
        add $10, %eax
        mov %eax, -16(%ebp)

# 0 "" 2
#NO_APP
        movl    -12(%ebp), %edx
        xorl    %gs:20, %edx
        movl    -16(%ebp), %eax
        jne     .L5
        addl    $20, %esp
        popl    %ecx
        .cfi_remember_state
        .cfi_restore 1
        .cfi_def_cfa 1, 0
        popl    %ebp
        .cfi_restore 5
        leal    -4(%ecx), %esp
        .cfi_def_cfa 4, 4
        ret
.L5:
        .cfi_restore_state
        call    __stack_chk_fail
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .section        .text.unlikely
.LCOLDE0:
        .section        .text.startup
.LHOTE0:
        .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
        .section        .note.GNU-stack,"",@progbits

인라인 어셈블리 소스코드 예제는 10줄밖에 안되는데 어셈블리어로 변환시키니 58줄이나 되어버려 복잡해 보이는데, 잘 살펴보면 인라인 어셈블리어 부분은 다음과 같은 부분에 해당된다. 원본 인라인 어셈블리 소스가 3줄이었는데 이것이 gcc에 의해 변환해서 얻어진 어셈블리어 소스에서도 마찬가지로 3줄이다.

...(생략)
#APP
# 3 "test.c" 1
        mov $100, %eax
        add $10, %eax
        mov %eax, -16(%ebp)

# 0 "" 2
#NO_APP
...(이하 생략)

gcc 대신에 clang을 사용하면 어떤 결과가 나올까? clang으로 변환해서 얻어진 어셈블리어 소스는 다음과 같다. 출력 소스를 최대한 간단히 보기 위해서 -O2 옵션을 주어 보았다. (clang 3.8.0)

$ clang -O2 -m32 -S test.c
        .text
        .file   "test.c"
        .globl  main
        .align  16, 0x90
        .type   main,@function
main:                                   # @main
# BB#0:
        pushl   %eax
        movl    $0, (%esp)
        #APP
        movl    $100, %eax
        addl    $10, %eax
        movl    %eax, (%esp)

        #NO_APP
        movl    (%esp), %eax
        popl    %ecx
        retl
.Lfunc_end0:
        .size   main, .Lfunc_end0-main


        .ident  "clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)"
        .section        ".note.GNU-stack","",@progbits

자세히 살펴보면 인라인 어셈블리 부분의 명령 코드가 조금 달라졌음도 엿볼 수 있는데, 원래의 명령 코드는 mov였는데 movl로 바뀌는 등의 차이점도 있음을 알 수 있다. (movl의 l suffix는 long (32비트) 오퍼랜드에 대응하는 mov OP임을 뜻한다. l 이외에 b, s, w, q, t등의 suffix가 올 수 있다. 좀 더 자세한 내용은 https://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax 문서를 참고할 수 있다.

아무튼 clang의 경우 그 결과물이 훨씬 간단하니 이에 간단한 설명을 달아보자면 다음과 같다.

.text .file "test.c" .globl main .align 16, 0x90 .type main,@function main: # @main # BB#0: pushl %eax # %eax 레지스터를 스택에 넣어 스택이 하나 쌓았다. 하나의 스택 공간 확보. movl $0, (%esp) # 스택 최상위 주소"(%esp)"에 0을 넣음. # 해당 스택위치 자리를 0으로 초기화 한 것은 지역 변수 dst를 0으로 초기화 한 것에 해당 #APP movl $100, %eax # %eax 레지스터에 100을 넣음. addl $10, %eax # %eax 레지스터에 10을 더함 movl %eax, (%esp) # %eax 레지스터의 내용을 "(%esp)" 스택 최상위 주소에 넣음. # 해당 위치는 0으로 초기화 되어있었음 #NO_APP movl (%esp), %eax # 스택 최상위 주소의 내용"(%esp)"을 %eax 레지스터에 넣음. popl %ecx # 최상위 스택을 비움. 하나 쌓여있던 스택을 버림. retl # main() 함수 리턴/종료. %eax 레지스터에 리턴값이 보존되어 있다. .Lfunc_end0: .size main, .Lfunc_end0-main .ident "clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)" .section ".note.GNU-stack","",@progbits

여기에 조금 더 부연설명을 추가하자면, 메인 함수가 시작하면서 pushl %eax를 실행하여 스택을 하나 늘리고 해당 스택을 0으로 초기화 하고 있는데, 이는 "int dst = 0;" 소스가 어셈블리어로 변환된 것에 해당한다. 즉, c 소스 main()함수에서 지정된 지역 저장소 "int dst"를 위해 스택을 하나 늘리고 이것을 0으로 초기화 한 것이다.
그런 다음에 이어지는 인라인 어셈블리 부분이 실행되고 나면 해당 지역 저장소 dst의 위치인 스택 값이 %eax 레지스터에 옮겨서 저장되는데, 메인함수 main()이 리턴하는 값은 %eax 레지스터에 최종적으로 전달된다.

(스택은 함수의 인자 전달 및 지역 변수의 용도 등등으로 활용되는데, push를 하면 스택이 쌓이며, 스택 레지스터 esp는 감소를 하게 되며, ebp 주소를 기준으로 [ebp + offset] 형태의 주소로 스택을 지정하여 사용하게 된다. 초보자는 조금 헷갈릴 수도 있지만 스택이 쌓이는 방향에 유의해야 하는데, 메모리 상위 주소가 아래쪽인 경우 스택이 위쪽으로 쌓이고, 상위 주소가 위쪽이면 스택은 아래 방향으로 쌓인다.)

얻어진 어셈블리어 소스를 gcc 명령으로 컴파일해서 실행파일을 얻을 수도 있고, 이러한 과정을 거치지 않고 곧바로 실행을 해도 마찬가지 결과를 얻게 된다.

$ clang -m32 -O2 test.c
$ ./a.out ; echo $?
110
$
혹은
$ clang -m32 -O2 test.s
$ ./a.out ; echo $?
110
$

다시 처음으로 돌아가서 test.c 소스를 다음과 같이 조금 바꿔보자.

int main() {
        int dst = 0;
        __asm__ volatile (
                "mov $100, %%eax\n\t"
                "add $10, %%eax\n\t"
                "mov %%eax, %0\n\t"
                : "=r" (dst)
        );
        return dst;
}

즉, 출력을 지정하던 부분이 원래는 "=m" (dst) 였던 것을 "=r" (dst)로 바꾼 것인데, 여기서 "="는 출력을 뜻하고 "=m"는 메모리 유형의 출력을, "=r"은 레지스터 유형의 출력을 의미한다. 즉, 출력 유형이 메모리였던 것을 레지스터 유형으로 제한(Constraint)을 바꾼 것이다.
새로 바꾼 소스를 clang으로 다시 컴파일해서 얻은 어셈블러 소스는 다음과 같다. (소스파일 이름을 2.c로 저장해 clang -O2 -m32 -S 2.c 명령으로 컴파일 하였다.)

        .text
        .file   "2.c"
        .globl  main
        .align  16, 0x90
        .type   main,@function
main:                                   # @main
# BB#0:
        #APP
        movl    $100, %eax
        addl    $10, %eax
        movl    %eax, %eax

        #NO_APP
        retl
.Lfunc_end0:
        .size   main, .Lfunc_end0-main


        .ident  "clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)"
        .section        ".note.GNU-stack","",@progbits

와우! 어셈블리 소스가 매우 간단해졌다. 추가적으로 들어간 어셈블리어 소스가 전혀 없이 인라인 어셈블리 소스 단 3줄만 남아있다. 이 경우에는 gcc로 컴파일을 하더라도 대동소이한 결과물을 얻을 수 있었는데, 아래와 같았다. (add가 addl로 변환되는 등의 약간의 차이점이 있으나 이는 gcc/clang 컴파일러가 동일하게 해석한다.) gcc -O2 -m32 -S 2.c

...(생략)
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
#APP
# 3 "2.c" 1
        mov $100, %eax
        add $10, %eax
        mov %eax, %eax

# 0 "" 2
#NO_APP
        ret
...(생략)

인자의 유형 지정

그러면 이번에도 간단한 다른 예제를 살펴보자.

int main() {
    int src = 3;
    int dst = 5;
    __asm__ volatile (
        "imul %1, %0"
        : "=r" (dst) /* 출력 인자 지정 */
        : "r" (src) /* 입력 인자 지정 */
    );
    return dst;
}

이번에는 3과 5를 곱하고(imul 정수 곱셈 OP) 그 결과를 dst로 저장하려는 것이다. 이번에는 출력 인자와 입력 인자가 각각 하나씩 이며, src와 dst 유형 모두 레지스터로 지정하였고, 컴파일러에서 각 레지스터를 자동으로 지정할 수 있도록 해 보았다.
$ gcc -m32 -O2 -S test2.c 명령으로 컴파일해서 test2.s 소스를 살펴보니 다음과 같이 조금 이상하다. 컴파일을 하고 얻어진 어셈블리 소스가 의도한 대로 얻어지지 않고 있다. src와 dst 모두 %eax로 동일하게 지정되어 버린 것이다. (gcc / clang 모두 동일하며 아래는 gcc의 출력 결과.)

...생략
main:
.LFB0:
        .cfi_startproc
        movl    $3, %eax
#APP
# 4 "test2.c" 1
        imul %eax, %eax
# 0 "" 2
#NO_APP
        ret
...생략

그 이유는 출력 인자에 대한 제약 유형을 잘 못 지정했기 때문인 것인데, 출력 인자를 "=r" (dst)라고 하면 출력을 쓰기전용으로 지정하겠다는 뜻이기 때문이다. 의도대로 작동되려면 dst 값은 5이므로 읽기/쓰기가 모두 가능해야 한다. 따라서 출력 인자를 "=r"가 아닌 읽기/쓰기가 모두 가능한 변경자(Modifier) "+"를 써서 "+r"로 지정해야 한다. 즉, 소스를 조금 고쳐서 다음과 같이 해야 한다.

int main() { int src = 3; int dst = 5; __asm__ volatile ( "imul %1, %0" : "+r" (dst) /* 출력 인자를 읽기/쓰기 가능한 출력("+") 레지스터로 제한 */ : "r" (src) /* 입력 인자를 레지스터로 제한 */ ); return dst; }

이를 컴파일해서 소스코드를 얻으면 다음과 같다. (clang/gcc 모두 동일한 결과를 얻으며, 아래는 clang의 출력 결과)

...생략
main:                                   # @main
# BB#0:
        movl    $3, %ecx
        movl    $5, %eax
        #APP
        imull   %ecx, %eax
        #NO_APP
        retl
...생략

최종 확인하기 위해 컴파일 하고 실행을 하면 그 결과가 맞다는 것을 알 수 있다.

$ clang -m32 -O2  test2.c
$ ./a.out ; echo $?
15

정리

여기까지 인라인 어셈블리를 넣는 방식을 간단히 살펴보았는데, 정리해보면 인라인 어셈블리는 다음과 같은 형식으로 요약할 수 있다.

__asm__ volatile (
"인라인 어셈블리어 #1\n\t"
"인라인 어셈블리어 #2\n\t"
"인라인 어셈블리어 #3\n\t"
"인라인 어셈블리어 #4"
: 출력 인자1, 출력 인자 2, 출력 인자 3,
: 입력 인자4, 입력 인자5,
);

출력 인자의 유형을 "=r"라고 지정했을 경우 r은 레지스터 유형으로 제약(Constraint)을 하겠다는 것을 뜻하며, "+"는 읽기/쓰기 가능하게끔 제약 변경자(Constraint Modifier)를 걸어준다는 것을 뜻한다.

입출력 유형에 대한 제한/제약(Constraint)은 https://gcc.gnu.org/onlinedocs/gcc/Simple-Constraints.html 문서를 참고할 수 있으며,
추가적인 제약 변경자(Constraint Modifier)는 https://gcc.gnu.org/onlinedocs/gcc/Modifiers.html 문서를 통해 자세히 살펴볼 수 있다.

인라인 어셈블리에 대해 더 자세히 알고싶다면 다음 문서를 참고하길 바란다.

https://wiki.kldp.org/wiki.php/DocbookSgml/GCC_Inline_Assembly-KLDP

'입문하기' 카테고리의 다른 글

기타 케이블의 커패시턴스 효과  (4) 2022.07.11
x86 어셈블리어 배우기 - 리버스엔지니어링 기초  (0) 2017.03.01
아멘 샬롬  (0) 2015.04.21
아가페 프로토스  (1) 2015.04.20
by dumpcookie 2017. 3. 5. 16:53

오랫동안 프로그래밍을 해본 사용자라 할지라도 어셈블리어가 필요하거나 리버스엔지니어링(역공학)이 필요한 경우는 별로 많지 않다. 본인도 90년대에 처음 GW 베이직을 시작으로 터보파스칼/터보C를 사용하며 프로그래밍의 기초를 배웠던 것이 그 시작이었지만 어셈블리어를 사용할 기회는 그다지 많지 않았다. 터보 파스칼의 그래픽 라이브러리를 사용하다가 마이크로소프트웨어 잡지책에 나와있던 원그리기/직선 그리기 알고리즘을 MASM 어셈블리어로 구현하고 이를 개선하려고 하면서 VGA 메모리 접근을 위해 주소를 어셈블리어로 직접 다루던 부분을 디버깅하던 기억이 아직도 남아있다. 당시 어셈블리어는 이해하기 어려웠고 고치기도 힘들었으나 최적화되고 나서 기대했던 속도가 나왔을 당시의 느꼈던 감격은 어셈블러의 단점을 보상하고도 남음이 있었다.

아무튼 지금 이 글을 쓰고있는 시점에서 조차도 본인은 어셈블리어에 대한 경험이 별로 없지만, 어셈블리어 혹은 리버스엔지니어링에 대한 기초적인 입문서도 많지 않다는 것에 조금 의아스럽게 생각하며, 어셈블리어에 완전히 생초짜라도 누구나 쉽게 따라해보면서 어셈블리어의 기초와 리버스엔지니어링을 같이 배워가며 문서를 쓸 예정이다.

C언어 기초 및 리눅스/유닉스에 어느정도 익숙한 분들을 대상으로 하지만 내용은 훨씬 쉽게 쓰려고 한다.

gcc와 objdump를 사용한 간단한 역공학

가장 먼저 초간단 C 프로그램을 만들어보자.

int main() {
return 0;
}

이를 a.c로 저장하고 gcc -c a.c 라고 하면 a.o가 얻어지며 a.o 오브젝트 파일을 objdump를 사용하여 간단히 역어셈블해보면 다음과 같다. (여기서는 -O1 컴파일 옵션을 주었으며, objdump -d 명령으로 디스어셈블을 하되 -Mintel 옵션으로 인텔 방식의 명령을 사용했다. gcc 버전은 5.4.0 / OS는 우분투 16.04.4)

$ gcc -c -O1 a.c
$ objdump -d -Mintel a.o

a.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   b8 00 00 00 00          mov    eax,0x0
   5:   c3                      ret

여기 처음으로 두개의 어셈블리어 명령이 보이는데, mov eax,0x0 명령은 eax 레지스터에 0x0을 옮기고(mov), ret 명령은 리턴하라는 것이다. (여기서는 eax레지스터를 처음으로 보게 되는데, 인텔의 16비트 레지스터는 ax,bx,cx,dx였던 것이 32비트로 넘어오면서 eax,ebx,ecx,edx로 확장되었고 ax,bx,cx,dx는 각각 8비트 레지스터 ah/al,bh/bl,ch/cl,dh/dl 등으로 구성된다. x86에서 지원하고 있는 레지스터에 대한 좀 더 자세한 정보는 위키피디아 https://en.wikipedia.org/wiki/X86#x86_registers 문서를 참고)

gcc에 최적화 옵션을 -O2로 주어서 컴파일한 후에 디스어셈블해보면 다음과 같다.

$ objdump -d -Mintel a.o

a.o:     file format elf64-x86-64


Disassembly of section .text.startup:

0000000000000000 <main>:
   0:   31 c0                   xor    eax,eax
   2:   c3                      ret

O2로 최적화를 하는 경우에는 오브젝트의 크기가 6바이트에서 3바이트로 줄어든 것을 볼 수 있는데, xor 명령을 사용하면 mov eax,0x0 명령이 5바이트인 것에 비해서 xor 명령은 2바이트밖에 안되면서 eax 레지스터의 내용을 지워서 0으로 만드는 동일한 작업을 수행하고 있다.

그렇다면 다음을 역어셈블해보면 어떻게 될까?

int main() {
return -1;
}

gcc -O2로 컴파일 한 후에 objdump -d -Mintel로 역어셈블 해보면 다음과 같다.

0000000000000000 <main>:
   0:   b8 ff ff ff ff          mov    eax,0xffffffff
   5:   c3                      ret

mov eax,0xffffffff 명령은 -1을 eax로 옮기는(mov)명령인데, -1는 32비트(4바이트)로 0xffffffff에 해당하며 mov eax, -1 명령과 똑같다.

NASM/YASM 소스 생성하기

이렇게 objdump를 이용해서 어셈블러 명령을 볼 수 있는 방법을 알게 되었지만 이와 같은 원리를 이용해서 컴파일 가능한 어셈블리어 소스를 만드는 것도 가능하다. (물론 몇가지 제한이 있다.) 다음 링크에서 찾아볼 수 있는 objconv를 사용하면 된다.

※ 다음 링크를 보면 gcc+objconv를 사용하여 nasm/yasm 소스를 만드는 방법을 볼 수 있는데, 이 내용을 참고하였다. http://stackoverflow.com/questions/35102193/how-to-generate-assembly-code-with-gcc-that-can-be-compiled-with-nasm

objconv 실행 파일은 다음 사이트를 통해 받을 수 있는데 리눅스에서 사용하려면 직접 컴파일 해야 한다. 소스 제공 및 다운로드: http://www.agner.org/optimize/ (x86/x86_64를 위한 최적화 자료모음이 함께 있다.)

이제 위에서 만든 초간단 소스 a.c을 gcc로 컴파일 하여 오브젝트 파일 a.o을 만든 후에 이것을 objconv를 통해서 NASM/YASM으로 컴파일 할 수 있는 asm 소스파일을 만들어보자.

이 경우 -fno-asynchronous-unwind-tables 옵션을 통해서 몇가지 디버깅 정보를 생성하지 않는 것이 편리하다. (gcc man 페이지를 살펴보면 -fasynchronous-unwind-tables 옵션은 찾을 수 있는데 이 옵션에 대한 해설은없다.  이 옵션은 디버거 혹은 가비지 콜렉터에서 사용할 수 있는 DWARF 2 형식의 해석 테이블(unwind table)을 만들지 않도록 한다.)

$ gcc -fno-asynchronous-unwind-tables -O2 -c a.c

다음 명령을 통해 a.o 오브젝트 파일을 objconv를 통해서 asm 소스 파일로 변환한다. (-fnasm 혹은 -fyasm 옵션을 써서 NASM/YASM 형식으로 출력한다.)

$ objconv -fnasm a.o

(이렇게 하면 a.asm 소스 파일을 얻는다. 출력 파일 이름을 별도로 지정하려면 objconv -fnasm a.o my.asm 라고 하면 my.asm 소스파일을 얻게 된다)

얻은 asm 소스 파일의 내용은 다음과 같다.

; Disassembly of file: a.o
; Wed Mar  1 7:00:34 2017
; Mode: 64 bits
; Syntax: YASM/NASM
; Instruction set: 8086, x64

default rel

global main: function


SECTION .text   align=1 execute                         ; section number 1, code


SECTION .data   align=1 noexecute                       ; section number 2, data


SECTION .bss    align=1 noexecute                       ; section number 3, bss


SECTION .text.unlikely align=1 execute                  ; section number 4, code


SECTION .text.startup align=16 execute                  ; section number 5, code

main:   ; Function begin
        xor     eax, eax                                ; 0000 _ 31. C0
        ret                                             ; 0002 _ C3
; main End of function

이렇게 얻어진 위 소스는 몇가지 오류를 바로잡아야 NASM/YASM 명령을 통해 컴파일 할 수 있는데, sed 명령을 통해 다음과 같이 필터링해야 한다. (위의 stackoverflow에 나와있는 것을 참고로 해서 조금 수정한 sed 명령은 다음과 같다)

$ sed -i "s/align=1 //g;s/[a-z]*execute//g;s/: *function//g;/default *rel/d;" orig.asm

혹은 다음과 같이 불필요한 부분을 지우는 대신에 주석처리해서 남길 수도 있다. (";" 기호 뒤의 내용은 주석으로 인식됨)

$ sed -i "s/\(align=1\) /;\1 /g;s/\([a-z]*execute\)/;\1/g;s/: \(function\)/ ; \1/g;/default *rel/d;" a.asm

(-i 옵션을 사용하지 않으면 콘솔 화면으로 출력된다. 이를 쉘스크립트로 만들거나 해서 좀 더 쉽게 써먹을 수 있을 것이다.)

이렇게 필터링 처리한 소스는 다음과 같이 된다.

; Disassembly of file: a.o
; Wed Mar  1 1 7:00:34 2017
; Mode: 64 bits
; Syntax: YASM/NASM
; Instruction set: 8086, x64


global main


SECTION .text                            ; section number 1, code


SECTION .data                          ; section number 2, data


SECTION .bss                           ; section number 3, bss


SECTION .text.unlikely                   ; section number 4, code


SECTION .text.startup align=16                   ; section number 5, code

main:   ; Function begin
        xor     eax, eax                                ; 0000 _ 31. C0
        ret                                             ; 0002 _ C3
; main End of function

이 소스는 비어있는 섹션을 지워서 다음과 같이 더 간단히 만들 수도 있다.

; Disassembly of file: a.o
; Wed Mar  1 7:00:34 2017
; Mode: 64 bits
; Syntax: YASM/NASM
; Instruction set: 8086, x64

global main ; function
SECTION .text.startup                                   ; section number 5, code
main:   ; Function begin
        xor     eax, eax                                ; 0000 _ 31. C0
        ret                                             ; 0002 _ C3
; main End of function

이것을 YASM/NASM을 통해 컴파일해서 오브젝트 파일을 얻고 실행파일을 얻으려면 다음과 같이 한다.

$ yasm -f elf64 a.asm gcc a.o -o a.out

(이 경우 얻어지는 a.o을 objdump -d 명령으로 보려고 하면 내용이 출력이 안되는데 이 경우에는 objdump -D 옵션으로 출력하면 된다. objdump --help에 의하면 -D, --disassemble-all 이라는 설명을 볼 수 있다.) 이를 실행해보면 다음과 같다. (리턴값이 0임을 확인할 수 있다)

$ ./a.out ; echo $? 0

Hello World!

그러면 이제 조금 더 그럴듯한 C프로그램을 nasm으로 변환시켜보자.

#include <stdio.h>

int main() {
    printf("Hello World!\n");
    return 0;
}

이를 gcc로 컴파일하고 objconv로 nasm 소스로 변환시키고 수동으로 정리하면 다음과 같다.

; Disassembly of file: c.o
; Mode: 64 bits
; Syntax: YASM/NASM
; Instruction set: 8086, x64
;
; manually simplified

default rel
global main
extern puts                                             ; near

SECTION .rodata.str1.1
?_001:                                                  ; byte
        db 48H, 65H, 6CH, 6CH, 6FH, 20H, 57H, 6FH       ; 0000 _ Hello Wo
        db 72H, 6CH, 64H, 21H, 00H                      ; 0008 _ rld!.

SECTION .text.unlikely
SECTION .text.startup align=16
main:   ; Function begin
        sub     rsp, 8                                  ; 0000 _ 48: 83. EC, 08
        mov     edi, ?_001                              ; 0004 _ BF, 00000000(d)
        call    puts                                    ; 0009 _ E8, 00000000(rel)
        xor     eax, eax                                ; 000E _ 31. C0
        add     rsp, 8                                  ; 0010 _ 48: 83. C4, 08
        ret                                             ; 0014 _ C3
; main End of function

이를 컴파일하고 실행시키면 정상적으로 실행됨을 확인할 수 있다.

위 소스에서 문자열이 저장된 부분을 다음과 같이 좀 더 정리할 수도 있다.

; Disassembly of file: c.o
; Mode: 64 bits
; Syntax: YASM/NASM
; Instruction set: 8086, x64
;
; manually simplified

default rel
global main
extern puts                                             ; near

SECTION .rodata.str1.1
msg:
        db "Hello World!", 00h

SECTION .text.unlikely
SECTION .text.startup align=16
main:   ; Function begin
        sub     rsp, 8                                  ; 0000 _ 48: 83. EC, 08
        mov     edi, msg                              ; 0004 _ BF, 00000000(d)
        call    puts                                    ; 0009 _ E8, 00000000(rel)
        xor     eax, eax                                ; 000E _ 31. C0
        add     rsp, 8                                  ; 0010 _ 48: 83. C4, 08
        ret                                             ; 0014 _ C3
; main End of function

clang의 출력과 비교

이번에는 clang을 이용하여 동일한 소스를 컴파일하고 역어셈블 해보자. 순서의 차이 및 그밖의 차이가 약간 있으나 다음과 같이 거의 동일한 결과를 얻을 수 있다. (소스를 약간 정리함)

$ clang -fno-asynchronous-unwind-tables -O2 -c c.c

; Disassembly of file: c.o
; Wed Mar  1 11:13:03 2017
; Mode: 64 bits
; Syntax: YASM/NASM
; Instruction set: 8086, x64

global main
extern puts                                             ; near

SECTION .text   align=16                         ; section number 1, code

main:   ; Function begin
        push    rax                                     ; 0000 _ 50
        mov     edi, ?_001                              ; 0001 _ BF, 00000000(d)
        call    puts                                    ; 0006 _ E8, 00000000(rel)
        xor     eax, eax                                ; 000B _ 31. C0
        pop     rcx                                     ; 000D _ 59
        ret                                             ; 000E _ C3
; main End of function

SECTION .rodata.str1.1                 ; section number 2, const

?_001:                                                  ; byte
        db 48H, 65H, 6CH, 6CH, 6FH, 20H, 57H, 6FH       ; 0000 _ Hello Wo
        db 72H, 6CH, 64H, 21H, 00H                      ; 0008 _ rld!.ob

objdump와 gdb 비교

objdump -d 명령을 사용하여 간단히 디스어셈블 할 수 있지만 gdb도 역시 가능하다. objdump를 사용하면 명령행으로 간단히 디스어셈블을 할 수 있는 반면 gdb를 사용하면 옵션을 조절해가며 볼 수 있는 등의 장점이 있다.

gdb를 사용하기 위해서 위의 가장 간단한 프로그램 a.c를 다음과 같이 컴파일하고 gdb로 로드해보자.

$ gcc -O2 -g -c a.c
$ gdb a.o
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.o...done.
(gdb) _

gdb 파일명 명령을 내리면 파일이 실행파일/오브젝트 파일 등을 로드하고 (gdb) 프롬프트가 뜨고 명령을 기다린다.

이 상태에서 help 명령을 내리면 gdb에서 사용할 수 있는 몇가지 명령 목록이 나오는데, help all 등을 하면 모든 gdb 명령 목록을 보여주며, gdb 명령이 잘 생각이 나지 않는다면 탭키를 두어번 누르면 명령 목록이 주르륵 나온다.

우선 x86 사용자라면 NASM/YASM 스타일이 익숙할 것이므로 set disassembly-flavor intel로 인텔 스타일로 바꿔준다. 오브젝트 파일을 디스어셈블하려면 disassemble을 입력하고 탭을 몇번 치면 disassemble 명령을 통해 디스어셈블 가능한 함수 목록이 나온다. disassemble main라고 입력하면 objdump -d -Mintel 명령으로 디스어셈블 하듯 콘솔에 내용을 출력한다.

(gdb) disassemble (탭을 몇번 치면 다음과 같이 함수 목록이 나온다)
a.c   int   main (a.c 소스의 int main 함수)
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000000000 <+0>:     xor    %eax,%eax
   0x0000000000000002 <+2>:     retq
End of assembler dump.
(gdb) set disassembly-flavor intel (인텔 스타일로 출력)
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000000000 <+0>:     xor    eax,eax
   0x0000000000000002 <+2>:     ret
End of assembler dump.
(gdb)

a.c 소스를 -g 옵션을 넣어 디버깅 정보를 넣어주었으므로, gdb의 list 명령으로 소스를 같이 볼 수 있다.

(gdb) list
1       int main() {
2       return 0;
3       }
(gdb)

위에서 사용했던 set disassembly-flavor intel같은 옵션은 ~/.gdbinit 설정 파일에 저장해 두면 편리할 것이다. 즉 다음과 같은 내용을 .gdbinit 파일에 넣어둘 수 있다.

set disassembly-flavor intel
set history save on
set history size 1000
set history filename ~/.gdb_history

여기서는 gdb를 통해 간단히 디스어셈블을 하는 방법만 알아보았지만, 더 궁금한 사용자라면 gdb를 사용하려면 필수적인 몇가지 명령은 https://blogs.oracle.com/ksplice/entry/8_gdb_tricks_you_should 문서를 통해 볼 수 있다.

gdb의 사용법은 조금 복잡해보이지만 기본적인 사용법을 익히고 나면 어렵지만은 않다. 검색해보면 한글로 된 좋은 자료도 많으니 이를 참고할 수 있을 것이다.

※몇몇 참고문서

- Relocatable Global Data on x86 - http://nullprogram.com/blog/2016/12/23/

by dumpcookie 2017. 3. 1. 11:24

이번에는 히브리어 읽기를 도전해봅시다.

구약성경은 히브리문자로 쓰여졌으며 독특해보이는 알파벳을 가지고 있습니다.

from 위키백과

AlefGimelDaletZayinHetTetYodKaf
אבגדהוזחטיכ
ך
LamedMemSamekhAyinTsadiQofReshTav
למנסעפצקרשת
םןףץ

그냥 보아도 도통 이해가지 않는 모양의 문자지만, 맨 처음 네개의 문자의 명칭이 의외로 알파벳과 유사한 것을 볼 수 있습니다. 알파 Aα는 Alef / 베타 는 Bet / 감마 Γγ는 Gimel / 델타 Δδ는 Dalet

사실 히브리문자는 고대 페니키아문자(기원전11세기)에서 갈라져나와 기원전10세기부터 사용되어졌다고 추정되고 있는 고대 히브리문자에서 비롯되었다고 합니다. 고대 히브리문자는 페니키아문자와 거의 비슷하게 생겼는데, 히브리문자는 이를 바탕으로 네모 모양으로 변형된 형태입니다. 자세한 모양은 위키백과 문서를 참조하세요.

알파A는 소 머리 모양에서 유래하였으며, 베타B는 집을 의미하며, 감마는 낙타(gimel, camel)을 의미하며, 델타Δ는 문(door)에서 유래했다고 하는데, 델타 삼각주는 델타의 모양과 유사해서 붙여진 이름이라고 하지요. (알파벳 ABC에서 왜 세번째 문자가 G가 아닌 C일까 의문을 가지신 분이 있다면, 원래는 낙타는 Gimel이었고 Camel이라고 발음이 변한 것을 상기해봅시다. 게다가 G는 C 문자에 감마의 Γ모양이 덧붙여진 형태입니다.)

아무튼 한번에 다 외우기는 어려울테니 알레프/베트/기멜/달레트 네개의 문자 모양을 우선 외워봅시다.

그러면 문자를 읽어볼 차례입니다.

אָלֶף 알레프

첫 문자부터 ABCD가 아닌 문자가 나와서 당황스럽습니다만, 히브리문자는 오른쪽에서부터 읽는 문자입니다. 그러므로 맨 오른쪽의 א알레프 문자를 먼저 읽는 것입니다. 그 다음 문자는 ל라메드(Lamed)인데 그리스문자 λ람다에 해당하며 자음 ㄹ에 대응합니다. 마지막 문자 ף는 위에서 살펴보면 Pe에 해당하며 그리스 문자 파이 π 알파벳 P에 해당합니다. 발음을 이어서 하면 아+ㄹ+ㅍ가 됩니다. 그런데 아래에 점이 보입니다. 아래 위로 붙이는 점은 모음의 발음을 뜻합니다. (한글도 작대기 앞 뒤 혹은 위 아래에 점을 붙여 발음이 바뀌는 형태라는 것을 상기해보면 재미있습니다.)

알레프 문자 아래에 붙은 T 모양은 모음 "아" 발음이며, 라메드 문자 아래의 세개의 점은 "에" 발음이고, 마지막 문자에는 모음 기호가 붙어있지 않습니다. 따라서 발음을 이어붙이면 아+ㄹ+ㅔ+ㅍ가 됩니다.

이제 히브리문자를 읽는 법을 이해했으니 다른 문자들도 읽어봅시다.

אָמֵן 아멘

히브리어를 어원으로 가지는 가장 유명한 단어일 것입니다. 알레프א가 첫 문자이고, 두번째 문자는 מ이 나오고 (한글 ㅁ과 모양이 유사하고 발음도 ㅁ 발음), 눈ן순서 입니다. 모음은 위에서 한차례 나온 T 받침이 아 발음이고, 멤מ 아래 모음점 두개는 "에" 발음이고 눈ן 문자는 "ㄴ" 발음입니다. 그래서 아멘이 됩니다. 그 뜻은 확신하다, 그러하다, 진실하다를 뜻합니다. 예수는 "내가 진실로 진실로 너희에게 말하는데"라는 말을 많이 썼는데, 여기서 "진실로"가 바로 아멘입니다.

שָׁלוֹם 샬롬

아마도 아멘 다음으로 유명한 히브리어일 것입니다. 첫문자 ש은 ㅅ 발음이고 두번째 문자는 앞에서도 나왔던 ל라메드 세번째 문자는 ו바브인데 v-w 발음이 나며, 이 경우에 위에 점이 하나 찍혀있고 o 발음이 됩니다. 마지막 문자는 ם멤이 또 나왔습니다. ש쉰의 오른쪽 위에 점이 찍혀있는데 이 경위 "쉬" 발음이며, 종합하면 쉬+ㅏ+ㄹ+ㅇ+ㅁ이 됩니다.
여기서 주의깊에 보면 멤은 두가지 폼이 있는 것을 볼 수 있습니다. 중간에 나오면 
מ 끝에 오면 ם이됩니다. 히브리문자는 다섯개의 모양이 살짝 다른 final 폼이 있습니다.

הים 하얌 - 바다

ה헷은 ㅎ, י요드는 y, 또 나온 ם멤은 ㅁ 발음이며 하얌이 됩니다. (헤엄치다의 헤엄과 발음이 유사)

אָדָם 아담 - Adam

이제 이것은 쉽게 읽을 수 있게 되었습니다. 받침 T모양은 "ㅏ"가 되고, 모음인 א알레프, ד달레트, ם멤은 "ㅇㄷㅁ" 이므로 아담이 됩니다.

히브리어를 배울 수 있는 사이트

다음 사이트에 가면 히브리어의 어원을 비롯하여 보기 쉽게 되어 있습니다. 아래 예시 이미지에서 모음이 빠져있는 것을 볼 수 있습니다. 히브리문자는 자음문자로서 모음이 생략되어 표기된 경우가 많습니다.

http://gifkg.org/hebrewword/adam.html



by dumpcookie 2015. 4. 21. 06:21


ἀγάπη 아가페 - 사랑


아가페아가페

아가페라는 단어는 많이 들어봤을 것입니다. 영어로 agape입니다. (발음이 일대일로 매칭되는 사실에 유의). 원어는 헬라어인데, 고대 그리스인이 쓰던 말입니다. (고대 그리스인은 자신들을 Έλλας(헬라스)로 불렀기 때문에 헬라어라 한다고 합니다. 한자 음역으로는 희랍어)

그러면 이제부터 그리스어/헬라어 읽는 방법을 살펴봅시다

조금 눈썰미가 있는 사람이라면 α알파 / γ감마 / π파이 정도는 알 것이고, η는 에타 입니다.
한글로 발음을 쓰자면 "아+ㄱ+아+ㅍ+에"가 됩니다.

그리스 알파벳은 아시다시피 로마자의 조상격입니다. (알파베타감마델타...Αα Ββ Γγ Δδ....)  (그리스 문자에 대해서 관심이 있으신 분이라면 한국 위키백과의 그리스 문자 문서를 참조하세요)

(감마는 대문자로 Γ인데, 재밌게도 한글로 ㄱ 과 발음이 동일하고 모양도 비슷합니다.)
에타
η는 그리스 대문자로 Η이며, 발음이 "에" 에 해당합니다.

아가페를 αγαπη로 쓰지 않고 ἀγάπη처럼 위에 점이나 획이 있는 것은 발음의 강약을 나타내는  ́(/상승) ̀(\하강) 혹은 발음의 방법(거친 숨소리 ̔ /약한 숨소리 ̓)을 나타냅니다.

그러면 다음을 살펴보죠.

πρῶτος 프로토스 - 처음의


프로토스 질롯프로토스 질롯 - from 위키백과


어디서 많이 들어 본 듯한 이 단어는 스타크래프트에서 프로토스 종족을 뜻하는 말이 아니라 원래는 "처음의"라는 뜻의 헬라어 입니다. 프로토스 종족의 뜻이 "첫번째 창조물"이라고 하니 헬라어를 차용해서 이름 붙인 것이지요.
이것도 읽어봅시다. ρ로, 로마자 R에 해당하고 R발음이고, ω오메가는 "알파와 오메가ΑΩ/αω"의 그 오메가이고, ㅗ 발음에 해당합니다. τ타우는 T발음, o오미크론은 약한 오 발음. ς시그마는 S발음인데 Σ대문자와 소문자σ와 더불어서 단어의 가장 맨 끝에 올 때에 ς로 쓰입니다. (음가 S에 해당하는 로마자 S에 더 비슷)

여기까지 읽은 분들이라면 글자와 발음이 일대일 대응되기 때문에, 그냥 소리내어 읽는 것을 조금 배우게 되면 읽는 것이 의외로 쉽다는 사실을 알 수 있습니다. 다음은 그리스어 알파벳과 발음입니다.

그러면 몇가지 상당히 익숙할 법한 단어를 살펴봅시다.

  • ἐκκλησία 에클레시아 - 교회
  • ἀνθρωπος 안드로포스 - 사람
  • ἀγγελός 앙겔로스 - 엔젤, 천사 - (아+ㄱㄱ+ㅔ+ㄹ+오+스 악겔로스라고 읽지 않고 앙겔로스라고 읽음에 유의. 즉 받침으로 위치할 때는 영어 G처럼 응 발음이 난다.)
    εὐαγγέλιον 에반겔리온(?) 유앙겔리온 - 복음, 윕실론 υ는 v와 유사하게 생겼으나 u 혹은 y발음이고, 람다
    λ는 ㄹ발음. ν뉴는 알파벳V의 소문자와 유사하게 생겼으나 N에 대응하고 발음도 이에 대응하며, 유명한 에니메이션 에반겔리온의 어원이기도 합니다.
  • ἀπόστολος 아포스또로스 - 사도 apostle: 고대어  ἀποστέλλω(아포스뗄로)에서 왔다고 하며, ἀπο(아포) "from"을 뜻하고 στέλλω(스뗄로) "I sent"를 뜻한다. (윅셔너리 참조)
  • λόγος 로고스 - 말씀.
  • κοσμος 코스모스 - 세상. 우주.
  • ειρήνη 에이레네 - 평강. (히브리어어로 샬롬)
  • χαρά 카라 - 기쁨.
  • παρά 파라 - para- 곁에
  • μικρός 미크로스 - 작은 (micro)

이 문서의 그리스어 입력은 http://www.typegreek.com/ 입력기를 이용했으며, 발음은 http://latina.bab2min.pe.kr/xe/hPron 사이트를 참조했습니다.


by dumpcookie 2015. 4. 20. 09:44
| 1 |