근 몇 년 만에 갑자기 재부팅이 안 되는 사태가 발생했다.

몇 번 자동 복구 및 안전부팅마저 실패하면 으례 윈도우 재설치를 하게 될 터였다. 이참에 SSD하나 구입하고 마음의 준비를 했지만, 수년간 아무 문제 없이 잘 쓰던 세팅이 날아가버리는 것에 대한 아쉬움도 많은 법... 고민 고민 하다가 구글링 시작...

- 이번 경우에는, 복구지점으로 복구가 잘 되지만, 마지막에 "레지스트리를 복구할 수 없다..." 어쩌고 하는 오류가 나며, 재부팅하면 무시무시한 critical process died라는 오류가 뜨며 재부팅,

 

CRITICAL PROCESS DIED.. -_-

- 안전모드 조차 작동하지 않음
- sfc /scannow, dism 명령을 통한 복구 및 bcdedit 등등 알려진 복구 방법들을 차례대로 시도해 봄.

이렇게.. 한참을 헤매다가, regedit.exe을 통해서 HIVE 로드/언로드 방식을 사용하면, 레지스트리를 살펴볼 수 있다는 점을 알아내고, 이를 통해 문제 있는 시스템 드라이브의 config를 살펴보니 c:\windows\system32\config\ 아래에 있는 HIVE 레지스트리 파일 중에 software 파일이 알 수 없는 이유로 깨져있다는 것을 발견. (복구지점으로 완료된 이후에, 레지스트리를 복구할 수 없다는 오류가 뜬다면, 아마도 이 HIVE파일에 문제가 있다는 것.)

- 복구 지점으로 복구를 시도할 때에 "이 드라이브에서 시스템 보호를 활성해야 합니다"라는 오류가 나오면서 복구지점 복구하기를 시도할 수 없다면 "rstrui.exe /offline:c:\windows=active" 명령을 사용하면 된다.
- 복구지점으로 복구가 거의 완료되고 나면 => 레지스트리 복구를 시도하는데, 이때 레지스트리를 복구할 수 없다는 메시지가 나오는 경우 => C:\windows\system32\config\ 아래에 있는 "system" 및 "software" HIVE 파일을 삭제 (백업한 후에 이름을 변경하거나 삭제) 한 후에 복구지점으로 복구하면 성공하게 된다. (본인은 chkdsk /r /f c: 등으로 체크 후, 깨진 것으로 확인되었던 "software" 파일을 이름 변경 후 복구지점 복구를 시도, 처음에는 실패했으나, 다음 시도에는 성공했다)
-  하드 드라이브가 여러개 붙어있는 경우, 간혹 C: D: E: 등의 드라이브 위치가 멋대로 바뀌는 경우가 있다. 본인의 경우, SATA2에 시스템을 설치, SATA1에 데이타를 설치하고 있었는데 (별생각 없이 설치했던 듯...), 커맨드라인 복구를 통해 들어가 DISKPART 명령으로 살펴보면 자꾸 C: E:의 위치가 바뀌어 있었다. diskpart 명령으로 원래의 위치로 드라이브명 변경을 한 후에 복구지점 복구를 시도해야 했다.

이러저러한 여러 방법을 시도 끝에, 겨우 복구지점으로 복구를 성공했다는 메시지가 뜨게 되었고, software HIVE 파일이 문제없이 복구된 것을 확인한 상태에서도 재부팅하니 여전히 화면은 먹통인 상태였다. 다시 차분한 마음으로, 안전모드로 부팅을 시도 => "critical process died" 오류는 다행히 없었으나, 검정 화면 상태에서 모니터가 아예 불이 들어오지 않음 => 재부팅 후 자동 복구를 시도하였으며, 상당한 시간이 흐른 뒤 자동복구가 완료되면서 다시 자동 재부팅 => 성공!!!!

이후, 제대로 시작 안 되는 몇몇 프로그램을 재설치하고, 시스템 복구지점을 만들고, 복구 용량을 20GB 정도로 늘려두었다.

이번 경우의 시스템 재부팅 문제의 원인은, 본인이 마침 registry를 건드렸던 부분이 있었는데, 그 부분이 알 수 없는 이유로 깨져있었으며, 불행 중 다행으로 복구가 가능했는데, 이 복구과정을 한 줄 요약해보면 다음과  같다.

HIVE 레지스트리 config파일을 regedit.exe 명령을 통해 이상유무를 확인하고 => 레지스트리 삭제 후 복구지점 복구 => regedit.exe으로 HIVE 레지스트리가 문제없는지 재확인 및 이상한 부분 수동 복구 후 HIVE파일 언로드 => 복구지점 복구 성공 => 안전모두로 부팅 후 먹통 => 자동복구 성공

한줄 요약: 의심스러운 설치 혹은 레지스트리를 수동으로 건들기 전에는 복구지점을 반드시 만들자!!

by dumpcookie 2023. 2. 15. 00:59

기타 픽업의 진동수 응답을 간단히 집에서 측정하는 방법이 없을까 검색해보니 DIY로 간단히 만들어 측정하는 방법이 있어서 소개해봅니다. 

http://kenwillmott.com/blog/archives/152

 

Electric Guitar Pickup Measurements | kenwillmott.com

Electric guitar pickups convert string movements into an electrical signal that can be heard through an amplifier. They come in many different designs, and have different electronic characteristics that give each pickup a different sound. It is possible to

kenwillmott.com

https://guitmod.wordpress.com/2016/09/26/diy-simple-measuring-of-a-pickups-frequency-response/

 

DIY: Simple Measurement of a Pickup’s Frequency Response

To judge the main sonic characteristics of an electric guitar pickup (resonance peak, resonance frequency, treble roll-off), you can easily measure its frequency response with the help of a few che…

guitmod.wordpress.com

필요한 것은 다음과 같습니다.

  1. 구형 형광등 등의 회로에서 추출한 인덕션 코일. (인덕터의 저항값은 10오옴 미만이며, L값은 수 미리 헨리 수준
  2. 100오옴 이하의 저항.
  3. 3.5mm 스테레오 짹 (못 쓰는 이어폰 등에서 잘라서 사용 가능)
  4. 오디오 인터페이스

인덕션 코일 준비

다음과 같은 작은 크기의 인덕터는 형광등 회로에서 추출할 수 있습니다.

형광등 회로에서 추출한 약 1.5x1.5x2cm 크기의 인덕터.
페라이트 코어를 분리

분리한 코일에 저항과 3.5mm 이어폰 짹을 붙이고 다음과 같은 식으로 회로를 구성해 만들어줍니다.

3.5mm 팁이 Letf 오디오 신호선, Sleeve는 그라운드선으로 연결 (Right 오디오 선은 절단)

이를 기타에 장착한 모습입니다. (참고한 사이트를 모두 함께 참고하시기 바랍니다) 코일을 눕히지 말고 세우면 유도전류가 더 잘 전달되어 소리가 커집니다.

집에 굴러다니는 일렉기타에 장착한 모습

측정 셋업

다음과 같이 측정을 위한 셋업을 합니다.

- 오디오 입력: 오디오 입력은 PC 혹은 오디오 인터페이스 3.5mm 헤드폰 짹을 통해서 기타의 출력은 오디오 인터페이스에 넣고, 3.5mm 짹 반대편 끝단의 인덕션 코일은 위의 그림과 같이 일렉 기타에 장착합니다.(일렉 기타의 측정하고 싶은 픽업에 장착. 코일은 세워서 장착하면 소리가 좀 더 커집니다.)
- 기타의 출력 TS짹에서 기타 케이블을 통하여 오디오 인터페이시 라인 입력과 연결시킵니다.
- 오디오 인터페이서 볼륨 및 기타 볼륨 레벨을 적절히 조절해줍니다.
- PC에서 REW를 실행시킵니다. REW는 룸 스피커 튜닝,  마이크 측정등등에 많이 사용하는 강력한 무료 소프트웨어입니다. 자세한 설명 방법은 생략.

출처 사이트를 살펴보면 전체적인 구성도를 볼 수 있으나, 약간 더 간단한 모식도를 그려보았습니다. 같이 참고하시기 바랍니다.

기타에 부착된 Pickup은 회로를 거의 생략한채로 그린 전체적 모식도

측정 결과

다음은 모사의 케이블을 사용한 Frequency sweep 측정 결과입니다. (LCR미터로 측정한 케이블의 커패시턴스 약 353pF) 거의 10여전 전에 구입한 케이블이나, 익스팬더로 마감되어 있고 스위치크래프트짹을 사용한 나쁘지 않은 케이블이었던 것으로 기억합니다. (총 길이가 2.9m인데 TS플러그의  커패시턴스값 16pF x 2 정도 빼주면 길이당 110pF/m정도 되는 비교적 양호한 케이블)

다음은 Sommer SC-Spirit XXL 케이블을 사용한 Frequency Sweep 측정 결과입니다. (뉴트릭 골드 도금. 커패시턴스 206pF. 케이블 길이 2m. 뉴트릭짹 커패시턴스 약 16pF, 케이블 자체 커패시턴스는 m당 ~87pF)

이 경우를 살펴보면 커패시턴스가 낮은 케이블의 피크 진동수가 4k근방에서 약간 오른쪽으로 치우친 것을 볼 수 있습니다. 즉, 커패시턴스가 낮은 케이블이 고음 손실이 덜하다는 것을 확인할 수 있습니다.

다음은 Treble POT을 조절해가며 얻은 그래프, Treble pot을 적당히 조절하여 낮추면 피크지점을 낮춰줍니다.

다음은 볼륨 레벨을 조절해가며 얻은 그래프입니다. 볼륨 레벨을 낮추면 피크지점이 오른쪽으로 조금씩 옮겨집니다.

다음은 노이즈 레벨을 측정한 그래프입니다. 첫번째 싱글픽업에 대한 노이즈입니다. 60Hz 노이즈 및 이에 대한 배음이 노이즈로 측정되고 있습니다. (접지된 상태. 접지하지 않으면 이보다 노이즈가 조금 더 들어옴)

싱글-싱글 픽업상태에서 노이즈가 가장 낮으며 다음과 같았습니다.

'DIY' 카테고리의 다른 글

DIY/Mod 콘덴서 마이크  (0) 2022.06.13
by dumpcookie 2022. 7. 14. 23:18

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

- 기타 케이블의 가장 중요한 포인트는 케이블 자체 커패시턴스이다.
- 일반적인 카나레/모가미 등등의 케이블 자체 커패시턴스는 대략 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

DIY 콘덴서 마이크에 관해 검색해보면 해외 사이트는 상당히 많이 나오는 반면, 국내에는 거의 자료가 없어서 본인이 최근에 모은 자료를 정리하고자 한다.

콘덴서 마이크는 일반 다이나믹 마이크와는 다르게 가격이 천차만별이다. 인기있는 슈어 SM58같은 다이나믹 마이크는 10만원대라 크게 부담이 없는 반면, 콘덴서 마이크를 찾아보면 적게는 3만원대부터 시작해서, 전문적인 콘덴서 마이크를  찾아보면 20만원대는 흔하고, 50만원 100만원대도 있고 최고로 알려져있는 노이만(Neumann) u87 Ai는 400~500만원대나 된다.

아무튼, 콘덴서 마이크를 써야 할 상황이 되어서 이리 저리 검색해보다가, DIY 콘덴서 마이크에 대한 정보를 찾게 되었고, 특히나 극강의 가성비인 베링거 ECM8000 마이크 및 이를 변경(Mod)하는 방법과 파나소닉 WM-61A Electret 콘덴서 마이크 캡슐에 대한 수많은 정보가 있다는 사실을 알게되었다.

극강 가성비의 베링거 ECM8000

베링거는 저렴하고 가성비 높은 제품을 판매하는 것으로 유명하다. ECM8000 측정용 콘덴서 마이크도 그중 하나이다. ECM8000관련 정보를 검색해보면 무수한 Mod 관련 정보를 찾을 수 있는데 몇가지를 요약해보면 다음과 같다.

※ 참고 링크: https://homerecording.com/bbs/threads/ecm8000-mods.111051/

 

ECM8000 Mods

OK folks, Today I went to local GC and got ECM8000--just to play with it and to see what it's capable of. Of course, before the listening, the very first thing I opened it up. I swear, I remember seeing somewhere on forums a picture of its PCB with tiny tr

homerecording.com

https://ameblo.jp/shin-aiai/entry-11230219227.html (ECM8000 여러 Mod 버전 설명. 일본어 주의)

  • ECM8000 초기 모델은 (출시일은 2004년경) 파나소닉의 WM-61A 혹은 이와 매우 유사한 콘덴서 마이크 캡슐을 사용했었다. (최근 모델은 WM-61a 유사품)
  • ECM8000 초기 모델은 트랜스포머를 내장했으나 지금은 트랜지스터를 통해 발란스 출력을 지원한다.
  • ECM8000 PCB 기판(PCB) 버전도 시기에 따라서 조금씩 다르나 그 회로는 변동이 없다. 이에 대한 커패시터/저항/트랜지스터 등의 부품 변경/제거등을 통한 개선 방법이 상당히 널리 알려져 있다.
  • 최근의 ECM8000 PCB기판은 거의 SMD 부품으로 바뀌어서 손쉬운 Mod가 어려워졌다.
  • 최근의 ECM8000 마이크 캡슐은 WM-61A와 비슷한 모양이나 노이즈가 상당하다. 이것만 WM-61A로 바꿔도 노이즈가 상당히 줄어든다.
  • ECM8000 PCB 회로는 이와 매우 유사하거나 거의 외관까지 복제인 제품이 많은데 게중 유명한 제품으로는 소나웍스 측정용 마이크가 있다. (https://www.clien.net/service/board/park/16603067 소나웍스는 카피 제품임에도 가격이 상당하다. 그 대신에 캘리브레이션 데이터 제공함)
  • ECM8000은 측정용 마이크로 판매되고 있으나, WM-61a 캡슐 변경 + Mod를 통한 노이즈 개선 및 출력 임피던스등을 낮춰주면 상당히 우수한 녹음용 콘덴서 마이크로 사용 가능하다.

전설의 WM-61A 콘덴서 마이크 캡슐

ECM8000  초기 모델이 사용했던 것으로 추정되는 파나소닉 WM-61A는 현재 생산 중단된 상태이나, 이에 대한 카피 제품을 알리를 통해 쉽게 구입할 수 있으며, WM-61A에 대한 수많은 문서들을 찾아볼 수 있다. 현재 WM-61A는 알리 익스프레서를 통해 2천원 정도에 구입할 수 있다. (알리 제품이라 쉽게 믿을 수는 없으니 제품 평가 및 사진 정보를 잘 보고 구입하기 바란다.) WM-61A의 특징은 다음과 같다.

Image from http://diy-fever.com/misc/wm-61a-microphone/

  • WM61A는 매우 저렴하다.
  • 20Hz~20kHz 주파수 응답 특성이 매우 flat하다.
  • 감도가 -35dB(±4dB) / SN ratio가 ~65dB근처로 무난하다.
  • ~100kHz 초음파 녹음도 가능하다고 한다.
  • 이러한 특징으로 인하여 WM-61A를 이용한 자작 콘덴서 마이크 문서 및 Mod에 대한 문서가 상당히 많다.

(WM61A는 Linkwitz 모드를 통해 (https://www.linkwitzlab.com/images/graphics/microph2.gif 이미지 참고) source follower로 개조하는 방법도 눈여겨볼 만 하다)

ECM8000 마이크 PCB 회로도 및 Mod

ECM8000 PCB 기본 회로도 분석

아래 링크는 ECM8000 회로도 분석을 위해 도움이 될만한 사이트이다.

 

Electret Microphones

Copyright Notice. This article, including but not limited to all text and diagrams, is the intellectual property of Rod Elliott, and is © 2015.  Reproduction or re-publication by any means whatsoever, whether electronic, mechanical or electro- mechanical

sound-au.com

ECM8000 Mod / DIY

최근에 나온 ECM8000 PCB 회로는 SMD 부품으로 구성되어 있어서 손쉬운 Mod가 어렵다. Mod를 손쉽게 하고, 마음에 드는 부품으로 바꾸려면 아예 PCB를 재구성 하여 만드는 것이 편리할 수 있다. 요새는 EasyEDA같은 사이트가 있어서 회로를 구성하고 이를 실제 PCB로 만들어 거버파일을 얻고 이를 다른 사람과도 손쉽게 공유할 수 있게 되었다. 다음은 본인이 직접 만들어본 PCB 및 Mod를 위한 회로도이다. https://oshwlab.com/wkpark/ecm8000-clone

 

ecm8000 clone - EasyEDA open source hardware lab

 

oshwlab.com

  • 공개한 ecm8000 클론 회로는 Linkwitz Mod도 손쉽게 적용할 수 있도록 회로를 구성하였다.
  • 공개한 ecm8000 클론 회로는 단면 회로로 구성되어있다. easypcb에서는 단면/양면 모두 가격이 동일하니, 이를 바탕으로 개조해볼 수 있을 것이다.
  • ecm8000의 PCB 크기는 66mm x 14.5mm 혹은 이보다 조금 작게 만들어야 한다. PCB기판 두께는 1.6mm이면 꼭 맞다. (ecm8000 실제 기판 두께는 1.2mm)
  • JLCPCB에서 이를 주문하면 10~20개 PCB를 주문하면 5달러 정도이나, 배송비가 18달러 수준이다. (더 저렴한 배송도 있으나 이 경우 배송이 한달 넘게도 걸린다.)
  • 각종 부품은 eleparts.co.kr / ic114.com 등을 통해 구입하였다.
    • 저항은 1/4W 1% 오차 메탈 저항
    • WM61A는 알리에서 구입. - 10개를 주문해서 회로를 구성한 후에 주파수 특성을 측정해보니 부품간에 약간씩 편차가 있었다. 10개중 2개는 불량에 가까웠다. (불량으로 판단되는 2개는 THD 특성이 상대적으로 좋지 않았다)
    • 커패시터는 C1, C2, C3, C6, C7, C8, C9, C10 등은 적층세라믹콘덴서 사용하였다. (최신 ECM8000의 경우 C1, C3, C5 커패시터 SMD부품이 모두 탄탈)
    • Q1 (NPN)의 경우 2SC945P, Q2/Q3(PNP)의 경우 2SA970을 사용하였다. Q2,Q3는 매칭 트랜지스터를 골라서 사용하였다. 
    • 좀 더 자세한 설명은 다음 기회에..

원하는 부품으로 교체할 것이 아니라면 PCB를 직접 주문하지 않는다 하더라도, WM-61A 캡슐만 바꾸는 것 만으로도 노이즈가 상당히 줄어들므로 시도해볼만 하다.

 

'DIY' 카테고리의 다른 글

REW를 사용한 기타 픽업 응답 측정  (0) 2022.07.14
by dumpcookie 2022. 6. 13. 00:12

몇년간 방치했던 HDR-HC3 캠코더를 고쳐보기 위해 분해 및 조립했다가 E:91:01 오류(flash error)를 만났다.

원래는 LCD가 제대로 나오지 않아서, 연결 케이블 단선을 가장 의심하고, 이것저것 알아보던 차에, 같은 모델이 중고로 싸게 나온 매물이 있어서 몇년전 구입해서 썼는데, 고질적인 뚜껑이 닫히지 않는 문제때문에 임시 방편으로 간단히 수리해 썼었다가, 귀찮아서 방치했던 것이었다.

불과 몇년이 지났지만 이제는 유튜브등에 검색해보면 상당히 자세한 분해 영상이 있어서 분해를 따라해보고, LCD단선 케이블을 확인하고 알리에 주문을 넣었고, 또 다른 한대는 LCD가 멀쩡했으나 분해를 비교 확인하기 위해서 같이 분해했다가 재조립했는데 E:91:01 오류가 난 것이다. 이 오류를 검색해보면 flash error라고 하는데, 이를 검색해보면 캠 메뉴얼이 나오고, 여기에 설명이 나오길 "abnomal situation"을 예방하기 위해서 펌웨어 특정 영역에 표식을 하고 (이를테면 특정 펌웨어 주소에 "00"이 저장됨), 서비스 센터를 찾아가지 않는 이상 고치기 어렵다는 것이었다... 황당

그런데 좀 더 찾아보면 RM95 컨트롤러를 사용하면 펌웨어 특정 영역을 강제로 "01" 등으로 고치고 리셋을 하면 멀쩡해진다는 것.

lea.hamradio.si/~s51kq/DV-IN.HTM 사이트에 여러가지 유용한 내용이 나오는데, flash error를 고치는 방법을 종합해서 정리해보면

  • 복구를 위해 RM95 LANC 컨트롤러가 필요하나 꽤 비쌈 (중고나라에 가끔 싼 매물이 나오기도 하나 기다리기 어려움
  • RM95 LANC 컨트롤러가 없다면 위 사이트에 설명된 PC인터페이스 용 케이블을 자작한다. (재료값을 다 합해도 1만원 수준임)
    • 위 사이트에 보면 자작 케이블을 만들기 위한 회로도 등이 나와있다. 디x이스마트에서 구매함.
  • LANC 자작 케이블을 사용하려면 윈도우95/98에서 dvin lite를 사용해야 하는데, 윈도우10에서는 작동이 안됨
  • 다행히도 도스용 RM95EMUL이 작동한다. (다만, 자잘한 버그때문에 약간의 오작동을 보여주나, flash error 오류를 수정하는 것은 가능함)
  • 도스로 부팅해서 RM95EMUL을 실행 후 소니 HDR-HC3 을 LANC케이블로 PC와 연결하고 play 모드로 변경, 메뉴얼에 설명한 것처럼 펌웨어 2바이트를 수정 확인함 (아래에 설명)->리셋 버튼을 눌러줌.
 

Process After Fixing Flash Error - Sony HDR-HC3 Service Manual [Page 11] | ManualsLib

 

www.manualslib.com

  1. 페이지 7 / 주소 01에 75라고 들어있는지 확인함. 이 값이 75가 아니면 75로 수정해주고 write.
  2. 페이지 7 / 주소 00의 값이 00이라고 되어있는 것을 01로 고친 후에 write.
  3. 페이지 7 / 주소 02의 값이 01이라고 되어있는지 확인함. 위의 1) 2)단계가 성공하면 01이라고 바뀌어 있음
  4. 캠을 끄고 리셋 버튼을 눌러준다.

이 과정에서 온갖 뻘짓을 다 해봤는데, RM95EMUL 도스용 프로그램은 RM95 링크를 통해 받을 수 있다. (소스코드가 같이 들어있으며 어셈블러)

LANCManager라는 프로그램도 있는데, RM95EMUL과 매우 유사하게 작동하고, FreeDos의 문제인지, 약간의 오작동이 있었다. (위의 세번째 단계가 제대로 작동하지 않음) xoomer.virgilio.it/carlopra/lanc/

 

KKLANC

My activity is currently freeware if you want to contribute to this project, you can do it very simply using standard paypal: PayPal     Email me to discuss any technical  issue kksmafgp@tin.it

xoomer.virgilio.it

LPT포트가 있는 컴이 없어서 좀 낡은 방치된 컴을 정비하느라, dvin이 윈도우10에서 제대로 작동이 안되어 한참을 헤메다가 겨우 고친 것이라, 혹시 비슷한 경험을 하실 분도 있을 것 같아 기록으로 남겨둔다.

 

'자가수리' 카테고리의 다른 글

소니 HDR-HC3 뚜껑이 닫히지 않는 경우 조치법  (11) 2014.10.25
by dumpcookie 2021. 2. 21. 04:49


최근의 일련의 x86 어셈블리어 관련 글은 클론 리플레이어상에서 x86을 지원하기 위해 할 수 있는게 무엇일까 하고 살펴보던 차에 썼던 것이다. 이런 저런 관련 정보를 찾아보고 테스트를 해보니, x86을 위해 빌드한 공유 라이브러리의 TEXTRELs 문제를 고치기 위해서는 MMX/SSE 등등의 지원을 위해 들어가있는 꽤 오래된 어셈블리어 소스를 고쳐야만 하는 문제였다.

https://sourceforge.net/p/mpg123/bugs/168/

소스를 살펴보다가 알게된 사실중 하나는, x264/ffmpeg/mplayer/libvpx 등등의 프로젝트에서 꽤 오래전에 누군가가 만들었던 소스를 서로 의도하지 않게(?) 공유하고 있었다는 사실이었다. 최초 누가 만들었는지 조차도 명확하지 않지만 그에 대한 언급이 소스에 있으며,  또 다른 것으로는, 어셈블리어로 있던 소스를 인라인 어셈블리로 바꾸면서 c 소스로 바뀐 경우, 혹은 그 반대로 c 소스를 어셈블러 소스로 변환시킨 후에 손으로 최적화 한 흔적들도 보였다.

아무튼 TEXTREL 문제를 고치기 위해 PIC 코드로 고쳐야 하는 부분은 많지 않았으나, 암호같던 어셈블리어가 조금씩 눈에 들어오니 고치는 것이 가능하겠다 싶어서 어셈블리어 공부(?)/테스트 및 삽질을 같이하여 TEXTREL 문제를 제거한 패치를 만들게 되었다.

패치는 https://github.com/wkpark/mpg123/tree/textrels 링크를 통해 받고 테스트해볼 수 있다.

일단 간단히 테스트를 해볼 수 있는 것이 mpg123 -t 명령을 통해서 입력 mp3를 받아 디코딩하여 -w foo.wav 옵션으로 WAV 파일로 디코딩된 파일을 들어보는 것이었다. 패치를 거듭 살펴보고 최종적으로 빌드후 실행해서 core 덤프가 나면 어디가 문제인지 gdb로 몇차례 살펴보고, 더이상 core dump 안되고 실행이 된 후 디코딩된 wav파일이 정상적으로 소리가 들렸으며, readelf -d 명령을 통해서 TEXTREL이 더이상 보이지 않는 것을 확인하였다.

$ readelf -d src/libmpg123/.libs/libmpg123.so

Dynamic section at offset 0x56e20 contains 27 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libm.so.6]
 0x00000001 (NEEDED)                     Shared library: [libdl.so.2]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000e (SONAME)                     Library soname: [libmpg123.so.0]
 0x0000000c (INIT)                       0x2f08
 0x0000000d (FINI)                       0x441b4
 0x00000019 (INIT_ARRAY)                 0x57a74
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x57a78
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x138
 0x00000005 (STRTAB)                     0x1160
 0x00000006 (SYMTAB)                     0x5f0
 0x0000000a (STRSZ)                      2954 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000003 (PLTGOT)                     0x58000
 0x00000002 (PLTRELSZ)                   848 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x2bb8
 0x00000011 (REL)                        0x1f48
 0x00000012 (RELSZ)                      3184 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x1e58
 0x6fffffff (VERNEEDNUM)                 3
 0x6ffffff0 (VERSYM)                     0x1cea
 0x6ffffffa (RELCOUNT)                   392
 0x00000000 (NULL)                       0x0

$ readelf -d src/libmpg123/.libs/libmpg123.so |grep TEXT
$

이렇게 빌드된 mpg123 라이브러리를 클론 리플레이어에 x86 jni 라이브러리로 넣고 x86/x86_64 AOSP+vmware에서 테스트해보니, 전에는 TEXTRELs 문제로 정지되던 것이 이제는 실행이 잘 되었다.

이 패치는 mpg123 sourceforge 사이트에 보고하였으며, 몇번의 피드백과 테스트를 통해 MMX/3DNow/3dnowext/SSE 디코더를 모두 지원하게 되었으며, 고친 소스는 위에서 언급한 github 사이트를 통해 확인할 수 있다.

by dumpcookie 2017. 3. 14. 04:06

X86 32비트 어셈블리어 소스코드 예제를 통해서 TEXTREL 문제를 확인해보고, 이를 TEXTREL 문제 없는 어셈블리어 소스를 만들어보자. 먼저 다음과 같은 간단한 소스코드 예제를 보자.

#include <stdio.h>
int main() {
   puts("Hello World\n");
   return 0;
}

이 소스를 clang으로 컴파일하고, objconv를 통해 NASM/YASM 소스코드로 변환시켜 조금 정리하면 다음과 같다. (clang -m32 -O2 -c 옵션으로 컴파일)

global main: function
extern puts

SECTION .text
main:
        sub     esp, 12
        mov     dword [esp], .str
        call    puts
        xor     eax, eax
        add     esp, 12
        ret
; main End of function

SECTION .data

.str:
        db "Hello World", 0xa, 0x0

스택 조작하는 부분을 더 간단히 고쳐보면 다음과 같다.

global main: function
extern puts

SECTION .text
main:
        mov     eax, .str
        push    eax
        call    puts
        add     esp, 4
        xor     eax, eax
        ret

SECTION .data

.str:
        db "Hello World", 0xa, 0x0

이를 my.asm로 저장하고 yasm -fnasm my.asm으로 컴파일 후에 gcc -m32 my.o 명령으로 링크를 하면 실행이 잘 됨을 확인할 수 있다

인라인 어셈블리에 익숙해지려면 AT&T 스타일의 어셈블리어도 알아할 것이므로 -S 옵션으로 어셈블리어 소스를 얻고, 이를 정리해보면 다음과 같다.

        .text
        .globl  main
        .align  16
main:
        movl    $.str, %eax
        push    %eax
        calll   puts
        addl    $4, %esp
        xorl    %eax, %eax
        retl

        .section        .data
        .align  16
.str:
        .asciz  "Hello World"

일대일 대응이므로 이해하는데 어렵지 않을 것이다. 이 소스는 my2.s로 저장한 후에 gcc -m32 my2.s로 컴파일하면 실행이 잘 됨을 확인할 수 있다.

이렇게 얻어진 어셈블리어 소스를 오브젝트 파일로 만든 후에 공유 라이브러리를 만들면 어떻게 될까?
다음과 같은 명령으로 공유라이브러리를 얻을 수 있으며, TEXREL 문제 역시 확인할 수 있다.

$ gcc -Wl,--warn-shared-textrel -m32 -shared -Wl,-soname,my2.so -o /tmp/my2.so my.o
/usr/bin/ld: my.o: warning: relocation in readonly section `.text'
/usr/bin/ld: warning: creating a DT_TEXTREL in a shared object.

그렇다면 이와 같은 문제의 어셈블리어 소스의 TEXTREL 문제를 어떻게 해결할 수 있을까?

함수의 리로케이션

우선 objdump -r 명령을 통해 리로케이션 정보를 살펴보자. (readelf -r 명령과 거의 동일)

$ objdump -r my.o

my.o:     file format elf32-i386

RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000001 R_386_32          .data
00000007 R_386_PC32        puts

puts 함수의 리로케이션 정보를 고치기 위해서는 NASM의 경우 메뉴얼에 나온것처럼 다음과 같이 고치면 함수의 리로케이션 문제가 해결된다. 즉 함수 호출을 call puts로 하는 대신에 call puts wrt ..plt라고 해준다.

global main: function
extern puts

SECTION .text
main:
        mov     eax, .str
        push    eax
        call    puts wrt ..plt
        add     esp, 4
        xor     eax, eax
        ret

SECTION .data

.str:
        db "Hello World", 0xa, 0x0

이렇게 고친 후에 yasm -f elf my.asm으로 컴파일하고, objdump -r 명령으로 확인해보면 뭔가 달라졌음을 확인할 수 있다.

$ objdump -r my.o

my.o:     file format elf32-i386

RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000001 R_386_32          .data
00000007 R_386_PLT32       puts

즉, 리로케이션 테이블에 있는 puts 엔트리의 오프셋은 전혀 변화가 없는데, 리로케이션 타입이 R_386_PLT32로 바뀌었으며, 함수 puts에 대한 리로케이션 문제는 수정된 것이다.

AT&T 스타일의 소스의 경우는 다음과 같은 식으로 고쳐야 한다.

        .text
        .globl  main
        .align  16
main:
        movl    $.str, %eax
        push    %eax
        calll   puts@PLT
        addl    $4, %esp
        xorl    %eax, %eax
        retl

        .section        .data
        .align  16
.str:
        .asciz  "Hello World"

YASM/NASM에서는 조금 헷갈리게 만드는 wrt ..plt라는 지시자를 썼지만, AT&T 스타일에서는 왠지 한결 쉬운듯한 @PLT를 사용하고 있다. 이 경우 역시 gcc -m32 -c 명령으로 컴파일한 후에 gcc로 공유 라이브러리를 생성시켜보면 여전히 TEXTREL문제가 남아있음을 확인할 수 있는데, 이는 함수에 대한 리로케이션 뿐만 아니라, 데이터에 대한 리로케이션 정보도 바꿔줘야 하기 때문이다.

어셈블리어 소스는 @PLT 지시자를 통해서 고쳤으나, objdump -d를 통해서 바이너리 코드를 살펴보면 바뀌기 전 코드와 완전히 일치한다. @PLT같은 지시자는 리로케이션 테이블의 정보를 바꿔주는 역할을 할 뿐, 실제 실행 코드 (.text 섹션내 실행 코드) 자체는 변화가 없다.

리로케이션 테이블을 readelf -r 명령을 통해서 보면 다음과 같다.

$ readelf -r my.o

Relocation section '.rel.text' at offset 0x54 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000001  00000201 R_386_32          00000000   .data
00000007  00000604 R_386_PLT32       00000000   puts

오브젝트 파일을 xxd로 덤프해서 바이너리 파일을 비교해보면 다음과 같은 차이가 있는 것을 볼 수 있다. (아래쪽 변경점은 소스파일의 파일 이름이 fa.asm 에서 fb.asm으로 변한 정보)

--- a   2017-03-10 18:54:40.016601488 +0900
+++ b   2017-03-10 18:54:43.364574036 +0900
@@ -4,11 +4,11 @@
 00000030: 0700 0100 0000 0000 0000 0000 0000 0000  ................
 00000040: b800 0000 0050 e8fc ffff ff83 c404 31c0  .....P........1.
 00000050: c300 0000 0100 0000 0102 0000 0700 0000  ................
-00000060: 0406 0000 4865 6c6c 6f20 576f 726c 6421  ....Hello World!
+00000060: 0206 0000 4865 6c6c 6f20 576f 726c 6421  ....Hello World!
 00000070: 0a00 0000 002e 7465 7874 002e 6461 7461  ......text..data
 00000080: 002e 7265 6c2e 7465 7874 002e 7374 7274  ..rel.text..strt
 00000090: 6162 002e 7379 6d74 6162 002e 7368 7374  ab..symtab..shst
-000000a0: 7274 6162 0000 0000 0066 612e 6173 6d00  rtab.....fa.asm.
+000000a0: 7274 6162 0000 0000 0066 622e 6173 6d00  rtab.....fb.asm.
 000000b0: 6d61 696e 0070 7574 7300 0000 0000 0000  main.puts.......
 000000c0: 0000 0000 0000 0000 0000 0000 0100 0000  ................
 000000d0: 0000 0000 0000 0000 0400 f1ff 0000 0000  ................

데이터 리로케이션

다음은 데이터 리로케이션 정보를 넣어줄 차례이다. 그런데 이 경우는 x86_32비트의 경우 문제가 약간 복잡한데, 이유는 차차 설명하기로 한다.

먼저 64비트의 경우를 살펴보자. hello world 프로그램을 clang -O2 -S 옵션으로 소스를 얻고 정리를 해보면 다음과 같다.

        .text
        .globl  main
main:
        movl    $.str, %edi
        callq   puts
        xorl    %eax, %eax
        retq

        .section        .rodata
.str:
        .asciz  "Hello World"

여기서 movl $.str, %edi 코드는 .str 문자열의 주소값($.str)을 %edi 레지스터에 넣으라는 것이다. 이 명령은 leal .str, %edi와 하는 일이 같다.

64비트의 경우에는 함수 호출 규약이 더 간단한데, 스택으로 호출 인자를 넘기는 것이 아니라 rdi, rsi 등의 레지스터를 통해 인자를 넘기고 있다. 따라서 push 혹은 pop을 통해 인자를 넘기지 않고 곧바로 edi, esi 레지스터를 수정하게 된다.

이 프로그램의 리로케이션 문제를 해결하려 하면 다음과 같이 바꿔주어야 한다.

        .text
        .globl  main
main:
        leal    .str(%rip), %edi
        callq   puts@PLT
        xorl    %eax, %eax
        retq

        .section        .rodata
.str:
        .asciz  "Hello World"

32비트에서처럼 함수의 경우에는 @PLT 지시자를 넣었으나, 데이터 섹션의 .str 주소를 넘기는 부분은 leal .str(%rip), %edi를 사용했다.
이것은 단순히 지시자를 사용한 것이 아니며, X86_64(AMD64)에서 새로 도입된 레지스터 %rip를 사용해서 .str 문자열 주소를 넘겨준 것이다.

AT&T의 문법이 헷갈릴 수 있으니 이에 대응하는 YASM/NASM 어셈블리어로 살펴보자.

default rel

global main

extern puts

SECTION .text

main:
        lea     edi, [rel .str]
        call    puts wrt ..plt
        xor     eax, eax
        ret

SECTION .rodata

.str:
        db "Hello World"

default rel등등이 포함된 것을 제외하고는 32비트/64비트 프로그램 소스가 거의 같고, .str 문자열 주소를 넘겨주는 부분이 다른 것을 볼 수 있다.

문자열의 주소는 .str이며, .str의 주소의 내용은 [.str]인데, lea eax, [.str]이라고 쓰면 .str의 주소값을 넘겨주라는 것이다. (NASM/YASM에서 사용할 수 있는 몇몇 주소 지정 방식은 다음 링크를 통해서 볼 수 있다. https://www.tortall.net/projects/yasm/manual/html/nasm-effaddr.html#nasm-effaddr-riprel)

이렇게 문자열 .str의 주소를 넘겨주는 코드에 rel이라는 지시자를 넣으면 리로케이션 문제가 없는 오브젝트 파일이 생성된다. (NASM/YASM에서는 rip 레지스터를 직접 사용하면 오류가난다.) 이렇게 해서 생성된 코드의 바이트수도 조금 다르다는 것을 다음과 같이 확인할 수 있다.

$ objdump -d nopic.o

ffa-nopic.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:   8d 3c 25 00 00 00 00    lea    0x0,%edi
   7:   e8 00 00 00 00          callq  c <main+0xc>
   c:   31 c0                   xor    %eax,%eax
   e:   c3                      retq

$ objdump -d pic.o # 수정된 PIC 코드의 경우

ffa.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:   8d 3d 00 00 00 00       lea    0x0(%rip),%edi        # 6 <main+0x6>
   6:   e8 00 00 00 00          callq  b <main+0xb>
   b:   31 c0                   xor    %eax,%eax
   d:   c3                      retq

함수와는 다르게, 데이터의 주소를 상대적 주소로 바꿔주기 위해서 명령 코드 자체가 조금 바뀐 것을 알 수 있는데, 64비트에서 데이터 주소를 상대적 주소로 바꿔주기 위해서 %rip 레지스터를 사용한다. %rip 레지스터는 X86_32에서는 %eip이며, 16비트 32비트의 경우 %eip 어드레스의 값을 손쉽게 불러오지 못하였으나, X86_64(AMD64)부터는 %rip를 불러서 상대적 주소를 쉽게 계산할 수 있게 된 것이다.

그렇다면 X86 32비트에서는 데이터의 상대주소를 어떻게 구할까?

Call Pop 트릭

콜팝은 콜라+팝콘의 뜻이 아니고, 32비트에서 %eip 레지스터의 값을 가져오는 트릭이다. call + pop을 통해 %eip 레지스터 값을 읽는 방식을 구현할 수 있다. 32비트에서도 명령 카운트 레지스터가 존재하지만, 이것은 직접 읽기가 불가능한 레지스터이다. 그런데 함수를 콜하게 되면 스택에 리턴할 주소를 저장한다는 사실을 알고 있다. 스택에 저장되는 리턴할 주소가 바로 명령 카운터 레지스터 %eip의 값인 셈이다. 또한, 리턴할 주소는 바로 다음 명령이 실행될 위치이며, 이 경우 local:주소의 값이 된다. 따라서 아래와 같이 하면 %ebx 레지스터에 local:의 실제 주소값이 저장될 것이다.

    call local
local:
    pop %ebx

pop 대신에 mov (esp), %ebx라고 해도 된다. 그렇다면 이렇게 얻어진 %ebx 값은 local:의 실제 메모리에 올라간 진짜 주소가 된다는 것인데, 그렇다면 데이터가 적재된 곳의 주소 위치는 어떻게 구하게 될까? 64비트에서 lea .str(%rip), %eax라고 하면 되었듯이, %rip 대신에 %eip가 저장된 위치인 %ebx를 사용하여 leal .str(%ebx), %eax 라고 하면 될 것 같은데, 실제로는 이런 메커니즘을 지원하지 않으며, 다음과 같은 조금 아리송한 코드가 실제 사용된다. 즉,

    call local
local:
    pop %ebx
    leal .str(%ebx), %eax

라고 해야 하는 것이 아니라 다음과 같이 한다

    call local
local:
    pop %ebx
local2:
    addl $_GLOBAL_OFFSET_TABLE_ + (local2 - local), %ebx
    leal .str@GOTOFF(%ebx), %eax

알쏭달쏭한 이 코드를 gdb로 파헤쳐보면 내막을 알 수 있지만, 실제로 _GLOBAL_OFFSET_TABLE_ 이라는 주소의 값은 프로그램이 적재되기 이전까지 알 수 없는 값이고, 소스코드상의 _GLOBAL_OFFSET_TABLE_은 조금 다르게 해석 되어진다. (이 부분은 아래에서 살펴보겠다.) 소스를 컴파일해서 얻어진 오브젝트 파일은 $_GLOBAL_OFFSET_TABLE_값을 아직 알지 못하고, 최종 실행파일은 링커에 의해 해석되어진 후에 GLOBAL OFFSET TABLE이 만들어지게 되어 비로소 GLOBAL_OFFSET_TABLE을 알게 된다.
본인도 이 부분이 제대로 이해되지 않아서 어려워했으나, gdb를 통해 내막을 살펴보고서야 이해하게 되었다.

_GLOBAL_OFFSET_TABLE은 변수? 상수?

우선 다음의 소스코드를 살펴보자. 이것은 hello world 프로그램의 PIC 버전이다. clang -O2 -fPIC -m32 -S 옵션으로 컴파일해서 소스를 얻고 이를 정리한 것이다.

        .text
        .globl  main
main:
        call    .tmp0
.tmp0:
        popl    %ebx
.tmp1:
        addl    $_GLOBAL_OFFSET_TABLE_+(.tmp1-.tmp0), %ebx
        leal    .str@GOTOFF(%ebx), %eax
        movl    $_GLOBAL_OFFSET_TABLE_, %ebx ; 디버그용 코드
        movl    $_GLOBAL_OFFSET_TABLE_, %ebx ; 디버그용 코드
        push    %eax
        call    puts@PLT
        
        add     $4, %esp
        xorl    %eax, %eax
        retl

        .section        .rodata
.str:
        .asciz  "Hello World"

$_GLOBAL_OFFSET_TABLE값을 알기 위해서 중간에 디버그 코드를 넣었다. 이를 컴파일해서 오브젝트 파일 및 실행파일을 얻었다. 이를 objdump -d 명령으로 살펴보면 main 함수가 다음과 같다.

objdump -d 목적파일

00000000 <main>:
   0:   e8 00 00 00 00          call   5 <.tmp0>

00000005 <.tmp0>:
   5:   5b                      pop    %ebx

00000006 <.tmp1>:
   6:   81 c3 03 00 00 00       add    $0x3,%ebx
   c:   8d 83 00 00 00 00       lea    0x0(%ebx),%eax
  12:   bb 01 00 00 00          mov    $0x1,%ebx
  17:   bb 01 00 00 00          mov    $0x1,%ebx
  1c:   50                      push   %eax
  1d:   e8 fc ff ff ff          call   1e <.tmp1+0x18>
  22:   83 c4 04                add    $0x4,%esp
  25:   31 c0                   xor    %eax,%eax
  27:   c3                      ret

위의 내용을 잘 살펴보면 $_GLOBAL_OFFSET_TABLE_ 값이 들어가야 할 자리가 $0x1 이라는 값이 들어있다. 또한, $_GLOBAL_OFFSET_TABLE_+(.tmp1-.tmp0)가 들어가야 할 자리는 두 주소의 차이 (.tmp1-.tmp0) 값이 1이므로 0x02이어야 할 것 같은데 이상하게 $0x03이 들어있다. 그러면 실행파일을 덤프해보면 어떤가 살펴보자.

objdump -d 실행파일

...(생략)
0804840b <main>:
 804840b:       e8 00 00 00 00          call   8048410 <.tmp0>

08048410 <.tmp0>:
 8048410:       5b                      pop    %ebx

08048411 <.tmp1>:
 8048411:       81 c3 f0 1b 00 00       add    $0x1bf0,%ebx
 8048417:       8d 83 c0 e4 ff ff       lea    -0x1b40(%ebx),%eax
 804841d:       bb e3 1b 00 00          mov    $0x1be3,%ebx
 8048422:       bb de 1b 00 00          mov    $0x1bde,%ebx
 8048427:       50                      push   %eax
 8048428:       e8 b3 fe ff ff          call   80482e0 <puts@plt>
 804842d:       83 c4 04                add    $0x4,%esp
 8048430:       31 c0                   xor    %eax,%eax
 8048432:       c3                      ret
...(생략)
Disassembly of section .got.plt:

0804a000 <_GLOBAL_OFFSET_TABLE_>:
 804a000:       14 9f                   adc    $0x9f,%al
 804a002:       04 08                   add    $0x8,%al

실행파일에서는 $_GLOBAL_OFFSET_TABLE_이 들어갈 위치의 값이 모두 다른 값으로 채워져 있으며, $_GLOBAL_OFFSET_TABLE_+(.tmp1-.tmp0) 값에는 $0x1bf0(7152)가 들어가고, $_GLOBAL_OFFSET_TABLE_값이 들어갈 줄 알았던 곳은 각각 $0x1be3(7139) , $0x1bde(7134) 값이 채워져 있다. 그리고 정작 _GLOBAL_OFFSET_TABLE_ 주소는 0x0804a000값이다.
실제로 상수인줄 알았던 소스코드상에 들어간 $_GLOBAL_OFFSET_TABLE_ 값은 링커에 의해 고정된 값으로 해석되는 것이 아니라 .data 섹션과의 오프셋값으로 해석되어지고 있는 것이다.

따라서 실제 GLOBAL_OFFSET_TABLE 즉 실행파일이 메모리에 올라갔을 때에 .data 세그먼트의 위치를 가리키는 값은 다음과 같이 계산되어진다.

        GLOBAL_OFFSET_TABLE의 실제 위치 = 현재 실행 코드의 %eip + 현재 실행 코드와 GLOBAL_OFFSET_TABLE 주소와 오프셋(여기서는 $_GLOBAL_OFFSET_TABLE_)

아무튼 이 과정은 GNU as + GND ld의 합작 과정인 것인데, YASM/NASM에서는 이마져도 약간 다르게 보이지만 원리는 같다. 다음 코드는 YASM/NASM에서 GLOBAL_OFFSET_TABLE를 구하는 코드이다.

global main: function
extern puts
extern _GLOBAL_OFFSET_TABLE_ ; YASM/NASM에서는 이를 선언해주어야 한다.

SECTION .text
main:
        call .get_got
.get_got:
        pop     ebx
.tmp:
        add     ebx, _GLOBAL_OFFSET_TABLE_ + .tmp - .get_got ; AT&T 스타일의 어셈블리어와 같은 방식
        ;add    ebx, _GLOBAL_OFFSET_TABLE_ + $$ - .get_got wrt ..gotpc ; NASM/YASM 메뉴얼에 나와있는 방식.
        lea     eax, [.str + ebx wrt ..gotoff]
        push    eax
        call    puts wrt ..plt
        add     esp, 4
        xor     eax, eax
        ret

SECTION .data

.str:
        db "Hello World", 0xa, 0x0

이렇게 하여 이 소스를 컴파일하여 오브젝트 파일의 리로케이션 정보를 보면 다음과 같게 되며 TEXTRELs 문제가 없게 된다.

$ objdump -r test.o

test.o:     file format elf32-i386

RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000008 R_386_GOTPC       _GLOBAL_OFFSET_TABLE_
0000000e R_386_GOTOFF      .data
00000014 R_386_PLT32       puts

※참고:

http://nullprogram.com/blog/2016/12/23/ - GLOBAL_OFFSET_TABLE 및 x86에서 리로케이션에 대한 설명이 되어있다.

by dumpcookie 2017. 3. 11. 09:40

지금에야 64비트를 주로 쓰는 환경이므로 x86 32비트 환경에서 Text relocation문제가 크게 문제될 만한 사항은 아니지만, 안드로이드 6.0 이후로는 x86/arm 32비트의 경우, TEXTREL 문제가 있는 공유 라이브러리를 사용하는 경우 아예 실행조차 되지 않기때문에 (API 23 이상으로 빌드된 APK의 경우는 실행 안됨) 이 문제를 정리 차원에서 적도록 하겠다.

예를 들어 x264 / mpg123 / FFmpeg 등등의 꽤 널리 알려진 오픈소스 프로젝트의 경우, x86 32비트로 컴파일을 하면 TEXTRELs 문제가 있게 된다. 문제있는 공유라이브러리는 어떻게 찾을 수 있을까?

먼저 TEXTRELs 문제가 있는 라이브러리는 readelf, scanelf 등을 사용하여 찾을 수 있다.

예를 들어 FFmpeg의 libavcodec.so는 TEXTRELs 문제가 있으며 다음과 scanelf -T 명령을 이용하면 다음과 같은 식의 매우 긴 출력을 한다.

https://wiki.gentoo.org/wiki/Hardened/Textrels_Guide 참고

 $ scanelf -T libavcodec.so
 TYPE   TEXTRELS FILE
  libavcodec.so: (memory/data?) [0x845599] in (optimized out: previous put_cavs_qpel8_h_mmxext) [0x845560]
  libavcodec.so: (memory/data?) [0x8455EB] in (optimized out: previous put_cavs_qpel8_h_mmxext) [0x845560]
  libavcodec.so: (memory/data?) [0x845669] in (optimized out: previous avg_cavs_qpel8_h_mmxext) [0x845630]
  libavcodec.so: (memory/data?) [0x8456BB] in (optimized out: previous avg_cavs_qpel8_h_mmxext) [0x845630]
  libavcodec.so: (memory/data?) [0x845771] in (optimized out: previous avg_cavs_qpel8or16_v1_mmxext) [0x845700]
  libavcodec.so: (memory/data?) [0x84577B] in (optimized out: previous avg_cavs_qpel8or16_v1_mmxext) [0x845700]
  libavcodec.so: (memory/data?) [0x8457A8] in (optimized out: previous avg_cavs_qpel8or16_v1_mmxext) [0x845700]
  libavcodec.so: (memory/data?) [0x8457C7] in (optimized out: previous avg_cavs_qpel8or16_v1_mmxext) [0x845700]
  libavcodec.so: (memory/data?) [0x8457D1] in (optimized out: previous avg_cavs_qpel8or16_v1_mmxext) [0x845700]
  libavcodec.so: (memory/data?) [0x8457FE] in (optimized out: previous avg_cavs_qpel8or16_v1_mmxext) [0x845700]
  libavcodec.so: (memory/data?) [0x84581D] in (optimized out: previous avg_cavs_qpel8or16_v1_mmxext) [0x845700]
....

공유 라이브러리는 이미 리로케이션이 되어서 구체적으로 어떤 오브젝트 파일의 오류가 있는지는 알 수 없으나 TEXTRELs 문제가 있는지 없는지 정도는 간단히 파악할 수 있다.

readelf -a 혹은 readelf -d 명령을 사용하면 다음과 같은 더욱 요약된 정보를 볼 수 있다. -d 옵션은 --dynamic 옵션과 같고, dynamic 섹션 정보를 보여준다. -a 옵션은 모든 정보를 출력한다.

$ readelf -d x86/libavcodec.so

Dynamic section at offset 0xc3d81c contains 36 entries:
  Tag        Type                         Name/Value
 0x00000003 (PLTGOT)                     0xc3eb58
 0x00000002 (PLTRELSZ)                   2328 (bytes)
 0x00000017 (JMPREL)                     0x57748
 0x00000014 (PLTREL)                     REL
 0x00000011 (REL)                        0x20b90
 0x00000012 (RELSZ)                      224184 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffa (RELCOUNT)                   27939
 0x00000006 (SYMTAB)                     0x158
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000005 (STRTAB)                     0xba18
 0x0000000a (STRSZ)                      60344 (bytes)
 0x00000004 (HASH)                       0x1a5d0
 0x00000001 (NEEDED)                     Shared library: [libavutil.so]
 0x00000001 (NEEDED)                     Shared library: [libswresample.so]
 0x00000001 (NEEDED)                     Shared library: [libz.so]
 0x00000001 (NEEDED)                     Shared library: [libmp3lame.so]
 0x00000001 (NEEDED)                     Shared library: [libstdc++.so]
 0x00000001 (NEEDED)                     Shared library: [libm.so]
 0x00000001 (NEEDED)                     Shared library: [libc.so]
 0x00000001 (NEEDED)                     Shared library: [libdl.so]
 0x0000000e (SONAME)                     Library soname: [libavcodec.so]
 0x0000001a (FINI_ARRAY)                 0xc3cf94
 0x0000001c (FINI_ARRAYSZ)               8 (bytes)
 0x00000019 (INIT_ARRAY)                 0xc3e818
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x00000016 (TEXTREL)                    0x0
 0x00000010 (SYMBOLIC)                   0x0
 0x0000001e (FLAGS)                      SYMBOLIC TEXTREL BIND_NOW
 0x6ffffffb (FLAGS_1)                    Flags: NOW
 0x6ffffff0 (VERSYM)                     0x1f41c
 0x6ffffffc (VERDEF)                     0x20b34
 0x6ffffffd (VERDEFNUM)                  1
 0x6ffffffe (VERNEED)                    0x20b50
 0x6fffffff (VERNEEDNUM)                 2
 0x00000000 (NULL)                       0x0

여기서 0x00000016 (TEXTREL) 0x0에 해당하는 부분이 TEXTREL 문제가 있는 라이브러리라는 것을 보여준다.

scanelf 혹은 readelf를 통해서 TEXTRELs문제를 어느정도 파악했다고 하자. 그렇다면 구체적으로 어떤 소스가 TEXTRELs문제를 일으키는지는 어떻게 알 수 있을까?
우선 이 문제를 원본 소스가 있을때를 가정으로 한다면 의외로 어렵지 않게 찾아낼 수 있다.

x86 32비트 라이브러리 하나를 예로 들어보자. 여기서는 FFmpeg라이브러리를 골라보았다. FFmpeg 라이브러리를 32비트 x86용 공유 라이브러리 형태로 컴파일한 후에, 각각의 오브젝트 파일에 대해서 살펴보도록 하자.

readelf를 사용하면 공유라이브러리뿐만 아니라 오브젝트파일의 정보도 볼 수 있는데, 특히 strip되지 않는 오브젝트 파일의 경우에는 다음과 같은 정보를 볼 수 있다. FFmpeg 라이브러리의 avcodec 모듈 소스중에 하나인 gif.c를 컴파일해서 얻어진 오브젝트 파일에 대한 정보를 readelf -r 명령으로 보면 다음과 같다.

Relocation section '.rel.text.pick_palette_entry' at offset 0xe98c contains 5 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000000c  00004102 R_386_PC32        00000000   __x86.get_pc_thunk.bx
00000012  0000420a R_386_GOTPC       00000000   _GLOBAL_OFFSET_TABLE_
0000001f  00004303 R_386_GOT32       00000000   __stack_chk_guard
000000cb  00004303 R_386_GOT32       00000000   __stack_chk_guard
000000f4  00004402 R_386_PC32        00000000   __stack_chk_fail_local

Relocation section '.rel.text.gif_encode_close' at offset 0xe9b4 contains 6 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000003  00004102 R_386_PC32        00000000   __x86.get_pc_thunk.bx
00000009  0000420a R_386_GOTPC       00000000   _GLOBAL_OFFSET_TABLE_
0000001f  00004504 R_386_PLT32       00000000   av_freep
0000002a  00004504 R_386_PLT32       00000000   av_freep
00000042  00004604 R_386_PLT32       00000000   av_frame_free
0000004a  00004504 R_386_PLT32       00000000   av_freep

Relocation section '.rel.text.put_bits' at offset 0xe9e4 contains 4 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000000a  00004102 R_386_PC32        00000000   __x86.get_pc_thunk.bx
00000010  0000420a R_386_GOTPC       00000000   _GLOBAL_OFFSET_TABLE_
0000006a  00002a09 R_386_GOTOFF      00000000   .LC2
0000007a  00004704 R_386_PLT32       00000000   av_log
...(생략)

복잡해보이지만, 이 오브젝트 파일에는 그 심볼 타입이 R_386_PC32, R_386_GOTPC, R_386_GOTOFF, R_386_PLT32등등이 있다는 것 정도를 알 수 있다. c언어로 작성된 소스코드로 얻어진 오브젝트 파일은 (이 경우 -fPIC 옵션으로 컴파일 되어) TEXRELs문제가 없는 경우이다. 반면, x86에 최적화된 x86/fft.asm 어셈블리어 소스를 컴파일해서 얻은 오브젝트 파일의 경우에는 readelf -r 명령 출력이 다음과 같으며, 이 경우는 TEXTRELs 문제가 있는 경우이다.

$ readelf -r  fft.o

Relocation section '.rel.text' at offset 0x2db4 contains 273 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000015  0000a201 R_386_32          00000000   .rodata
0000001e  0000a201 R_386_32          00000000   .rodata
0000003a  0000a201 R_386_32          00000000   .rodata
00000052  0000a201 R_386_32          00000000   .rodata
000000a6  0000a201 R_386_32          00000000   .rodata
000000db  0000a201 R_386_32          00000000   .rodata
000000e4  0000a201 R_386_32          00000000   .rodata
00000100  0000a201 R_386_32          00000000   .rodata
00000118  0000a201 R_386_32          00000000   .rodata
00000148  0000a201 R_386_32          00000000   .rodata
00000150  0000a201 R_386_32          00000000   .rodata
000001f1  0000a201 R_386_32          00000000   .rodata
00000232  0000a201 R_386_32          00000000   .rodata
0000023a  0000a201 R_386_32          00000000   .rodata
0000028a  0000a701 R_386_32          00000000   ff_cos_32
00000292  0000a701 R_386_32          00000000   ff_cos_32
00000393  0000a201 R_386_32          00000000   .rodata
000003d3  0000a201 R_386_32          00000000   .rodata
00000411  0000a201 R_386_32          00000000   .rodata
...(중략)
0000176c  0000a701 R_386_32          00000000   ff_cos_32
0000179e  0000a801 R_386_32          00000000   ff_cos_64
000017d0  0000a901 R_386_32          00000000   ff_cos_128
00001800  0000aa01 R_386_32          00000000   ff_cos_256
00001830  0000ab01 R_386_32          00000000   ff_cos_512
00001860  0000ac01 R_386_32          00000000   ff_cos_1024
00001890  0000ad01 R_386_32          00000000   ff_cos_2048
000018c0  0000ae01 R_386_32          00000000   ff_cos_4096
...(생략)

위에서 나오는 ff_cos_* 심볼은 fft.asm 소스상에서 .data 섹션에 들어가는 여러 상수들이다. 이 데이터의 타입이 R_386_32라고 나오고 있다. 바로 이러한 경우에 TEXTREL문제가 나게 되는 것이다. 다른 형식의 타입(GOTOFF,GOTPC 등등)이 보이지를 않고 있다.

readelf뿐만 아니라, 오브젝트 파일에 대한 정보는 objdump를 사용해도 알 수 있는데 objdump -r 명령으로 fft.o 파일을 살펴보면 다음과 같고 readelf 명령의 결과와 거의 대동소이하다. (물론 이 경우 역시 strip되면 아무 정보도 나오지 않는다.)

 $ objdump -r fft.o

fft.o:     file format elf32-i386

RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000015 R_386_32          .rodata
0000001e R_386_32          .rodata
0000003a R_386_32          .rodata
00000052 R_386_32          .rodata
000000a6 R_386_32          .rodata
000000db R_386_32          .rodata
000000e4 R_386_32          .rodata
00000100 R_386_32          .rodata
00000118 R_386_32          .rodata
00000148 R_386_32          .rodata
00000150 R_386_32          .rodata
000001f1 R_386_32          .rodata
00000232 R_386_32          .rodata
0000023a R_386_32          .rodata
0000028a R_386_32          ff_cos_32
00000292 R_386_32          ff_cos_32
00000393 R_386_32          .rodata
000003d3 R_386_32          .rodata
00000411 R_386_32          .rodata
...(생략)

이렇게만 봐서는 어셈블리어 소스 fft.asm으로 얻은 오브젝트 파일과, 일반 c언어 소스로 얻은 gif.o 오브젝트 파일의 차이점을 발견하기 어렵지만, fft.o는 TEXTRELs문제가 있고, gif.o는 이 문제가 없는 경우이다. 그렇다면 그것을 어떻게 판별할 수 있을까?

gcc의 다음 명령을 사용하면 x86 / 32비트 공유 라이브러리를 만들 수 있다. (내부적으로 ld 명령이 실행된다.)
gcc -m32 -shared -fPIC -Wl,-soname,libmy.so -o /tmp/libmy.so -lc foo.o bar.o

위 명령은 foo.o bar.o 오브젝트 파일을 공유라이브러리 libmy.so라는 이름으로 생성시키는 명령이다. (-m32 옵션은 32비트 옵션, -shared 옵션은 ld명령으로 공유라이브러리를 생성하라는 옵션. -o는 출력 파일 지정 옵션)
이 명령을 응용하면, gcc의 -Wl,--warn-shared-textrel옵션을 함께 써서 TEXTRELs 문제를 가지는 것으로 추측되는 오브젝트 파일을 검사할 수 있다.
예를 들어 위에서 예를 들어 사용했던 gif.o 오브젝트 파일을 단독적으로 사용해서 공유 라이브러리를 만드는 명령은 다음과 같다.
(공유 라이브러리 이름은 임의로 my.so라고 하였고, 출력은 /tmp/mygif.so,

$ gcc -Wl,--warn-shared-textrel -m32 -shared -Wl,-soname,my.so -o /tmp/my.so gif.o

이 명령을 실행하면 아무런 오류도 없이 /tmp/my.so 공유 라이브러리가 생성되며, readelf -d 명령으로 TEXTRELS 문제를 살펴봐도 아무 문제가 없다.

반면 fft.o 오브젝트 파일에 대해 동일한 방식으로 임시 공유라이브러리를 생성하면 어떻게 될까?

$ gcc -Wl,--warn-shared-textrel -m32 -shared -Wl,-soname,my2.so -o /tmp/my2.so fft.o
fft.o: warning: relocation in readonly section `.text'

이 경우 위와 같은 오류가 나며 my2.so 공유 라이브러리가 생성되었고, readelf -d 명령으로 TEXTREL 문제를 살펴보면 다음과 같이 TEXTREL 오류가 있음을 알 수 있다.

$ readelf -d /tmp/my2.so |grep TEXT
 0x00000016 (TEXTREL)                    0x0

종종 공유라이브러리 생성이 안될 수도 있는데, 오브젝트 파일에 심볼을 발견할 수 없는 경우, 다른 오브젝트 파일이 필요한 경우가 그러한 경우이며, 이러한 오류 등으로 링크가 안되어 공유 라이브러리가 생성이 안될 수도 있지만 여전히 리로케이션 문제가 있다는 워닝은 볼 수 있다.

 $ gcc -Wl,--warn-shared-textrel -m32 -shared -Wl,-soname,my2.so -o /tmp/my2.so /tmp/foo.o
/usr/bin/ld: /tmp/foo.o: relocation R_386_GOTOFF against undefined symbol `print' can not be used when making a shared object
/usr/bin/ld: final link failed: Bad value
collect2: error: ld returned 1 exit status


by dumpcookie 2017. 3. 10. 17:35

지난번에는 간단한 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

앱이 실행이 안된다는 보고가 있어서 이 문제를 살펴보았더니 인텔 CPU용 안드로이드 x86/x86_64에서 실행되지 않는 문제였다. 리포팅된 로그에 남아있는 오류는 대략 다음과 같았다.

java.lang.UnsatisfiedLinkError: dlopen failed: \
"/data/app-lib/org.dumpcookie.ringdroidclone-1/libffmpeg_jni.so" has unexpected e_machine: 40

예를 들어 아수스에서 나온 트랜스포머 TF103C는 인텔 Atom CPU를 사용하는데 현재 native library가 arm만 지원하고 있기때문에 실행이 안되고 있던 것이었다.

클론 리플레이어를 x86 기기에 사용하려면 mpg123 / sonic / soundtouch / mpg123 / lame 등등의 각종 라이브러리를 x86 용으로 다시 빌드해야 했기때문에 이를 빌드하고 테스트하기 위하여 다음과 같은 삽질성 작업을 하였다.
- 우선 x86/x86_64를 빌드하기 위해서 삽질 시작. -> ndk-build를 사용하려니 여러 라이브러리 의존성때문에 빌드하기 어려운 난점 -> 라이브러리간 의존성 문제는 ndk-build 빌드 스크립트를 수정하는 방식으로 회피하고, 실제로 빌드하면서 발견되는 컴파일 오류를 바탕으로 Android.mk 픽스하여 arm/x86 ABI로 컴파일
- 이와 더불어 AOSP x86 (Android 6.0 브랜치)프로젝트에서 소스를 가져다가 AOSP 전체 빌드하고,
- AOSP x86 환경하여 여러 라이브러리를 빌드하면서 ndk-build를 통해 수정했던 Android.mk 재수정

안드로이드의 빌드시스템을 제대로 이해하고 시작한 작업이 아니였기때문에 꽤 삽질을 했는데, AOSP의 build/core/* 스크립트 및 ndk-build의 ndk/build/core/* 스크립트 분석 및 다음 문서를 통해서 안드로이드 빌드 시스템을 좀 더 이해할 수 있었다.

http://web.guohuiwang.com/technical-notes/androidndk1 - Mastering Android NDK Build System - Part 1
- http://www.netmite.com/android/mydroid/2.0/build/core/build-system.html - Android Build System

특히 위 링크의 마지막 문서를 보면 1997년에 만들어진 Miller의 Recursive Make Considered Harmful 이라는 make에 대한 흥미로운 문서를 만날 수 있다. (2002년 2008년 개정판(?)을 pdf로 볼 수 있다)

FFmpeg

ffmpeg + android로 검색해보면 나오는 다음의 문서를 통해서 automake/autoconf + cross compile 방식을 써서 Android.mk를 만드는 방법을 어느정도 살펴볼 수 있다.

안드로이드 펍의 ffmpeg NDK 컴파일 방법 강좌 시리즈

http://www.androidpub.com/1645684
http://www.androidpub.com/1646144
http://www.androidpub.com/1646529
http://www.androidpub.com/1646540

그러나 이 강좌를 보면 알겠지만, 처음 보는 개발자라면 조금 당황할 수도 있는데, ffmpeg 프로젝트 자체가 고유의 빌드시스템을 가지고 있을 뿐만 아니라 유연성을 제공하기 위한 다양한 configure 옵션이 있으며, 여러가지 외부 라이브러리와 의존성이 있기때문에 컴파일하는 작업조차 상당히 까다로운 작업이다.

하지만 불행중 다행으로(?) 안드로이드용 FFmpeg을 보다 손쉽게 할 수 있는 방법이 있다. 공식 AOSP는 FFmpeg을 포함하지 않고 있으나 CyanogenMod 소스는 FFmpeg을 포함하고 있다. (반면 Android-x86 프로젝트 소스도 FFmpeg을 포함하고 있으나, 이 소스는 CyanogenMod에서 거의 그대로 가져온 소스였고, make파일도 불완전했다.) 그런데 CM13/CM14 소스는 이미 x86을 지원하고 있었지만 YASM에 관련된 수정을 해야 Android-x86 소스트리에서 정상적으로 컴파일이 된다. 즉, AOSP/CM 최신 빌드 스크립트는 이미 YASM을 지원하고 있기 때문에 CM소스에 들어있는 ffmpeg의 x86+YASM관련 fix가 필요 없다. YASM에 관련된 fix를 제거하고 x86_64에 대한 지원을 추가 수정 해주면 x86뿐만 아니라 x86_64 역시 컴파일이 잘 된다.

mpg123

Android용 mpg123는 검색해보면 몇가지를 찾을 수 있지만 대개 최신 mpg123이 아니고 x86도 지원하지 않는다. 기존에 있던 Android.mk + configure + Makefile을 바탕으로 Android.mk를 만들고 AOSP 소스트리에서 컴파일을 할 수 있었다.

일반적인 automake/autoconf 방식은 configure를 통해서 config.h를 생성시켜야 하는데 호스트는 x86_64이고 타겟을 arm용으로 빌드하는 cross compile 방식을 써야 한다. configure를 통한 cross compile은 대개 잘 지원하기는 하지만 이 방식의 각각의 라이브러리마다 약간씩 차이가 있으므로 configure.* 파일과 Makefile.*의 내용을 잘 살펴보면서 Android.mk를 작성해야 한다. 이 부분은 따로 설명하고 https://github.com/wkpark/mpg123에 올려두었다.

ndk-build

위에서 기술한 방식으로 x86_64로 빌드해서 성공하기는 하였지만, 이번에는 최신 스마트폰을 대응하기 위해서 arm64에 대해서 또 다시 빌드하려고 하니 또 AOSP트리를 별도로 컴파일 해도 하나 하는 고민에 빠졌다. 물론 이런 식으로 별도의 CPU마다 AOSP 소스 트리를 따로 마련해도 되겠지만 이렇게 하는 것은 뭔가 낭비인 것 같아서 이번에는 ndk-build를 조금 고치고 AOSP식의 소스를 관리하면 편리할 것 같아 ndk-build 스크립트를 조금 고쳐보았다. (ndk-build의 장점은 무었보다도 APP_ABI := armeabi armeabi-v7a arm64-v8a x86 x86_64 라는 식으로 지정하여 각기 다른 CPU별 바이너리를 보다 쉽게(?) 빌드할 수 있다는 점일 것이다.)

ndk-build 빌드 방식이 AOSP 빌드시스템과 다르게 불편한 점은 바로 외부 라이브러리의 의존성 문제이다. 예를 들어서 FFmpeg을 빌드하기 위해서 외부 라이브러리 lame을 사용하는 경우에 lame 라이브러리에 대해서 import-module 방식을 쓰거나 ($(call import-module, external/lame)과 같은 줄을 Android.mk에 추가해 줌) 혹은 PREBUILT_SHARED_LIBRARY 방식을 써야 한다. 그러나 이는 AOSP식의 빌드시스템에 비교해서 여간 불편한게 아니다. 또한 import-module 방식을 쓰는 경우에는 이미 빌드한 외부 라이브러리가 다시 빌드되는 현상이 있었다(이는 ndk-build의 버그로 보인다. 그밖에, Android.mk를 고칠때마다 다시 빌드되는 현상도 버그로 보였다. NDK_OUT을 동일하게 지정해도 여전히 다시 빌드되는 현상은 마찬가지였다. 소스를 추적해보니 아니나다를까 해당 오브젝트 파일의 디펜던시에 Android.mk 및 Application.mk가 지정되어있었다. 이를 제거하면 Application.mk / Android.mk를 수정해도 재컴파일 되거나 하지 않았다. 만약 재컴파일 해야 하는 경우라면 -B 옵션을 넣어 ndk-build -B 라는 식으로 실행해야 한다.)

import-module을 사용하는 방식이 PREBUILT_SHARED_LIBRARY를 사용하는 것 보다 Android.mk를 덜 수정하는 방식이어서 더 나아보였지만 AOSP 빌드시스템과 비교해보면 너무도 불편한 것 이었다. 즉, AOSP 빌드 시스템의 경우 external 하위에 무수한 외부 라이브러리들의 의존성이 자동으로 판별되며, 이미 빌드된 외부 라이브러리를 그대로 재사용 하게 된다는 점이 ndk-build와 비교해서 가장 큰 차이점이었다.

그리하여 삽질끝에 ndk-build 스크립트의 다음과 같은 내용을 수정하였다.

  1. LOCAL_STATIC_LIBRARIES := liblame 을 넣으면 lame 외부 라이브러리가 이미 빌드된 것이 있는 경우 해당 모듈에 대한 스크립트 로컬 변수를 자동으로 할당하게끔 수정 : ndk-build의 경우 모든 LOCAL_MODULE 및 import 된 모듈에 대해서 __ndk_modules.*라는 내부 변수가 세팅되게 된다. import-module call은 단순히 해당 모듈의 Android.mk를 include하는 방식이였는데, import-module방식을 쓰지 않더라도 __ndk_modules.* 내부 변수가 자동으로 추가되게끔 수정하게끔 하니 import-module 라인을 추가하지 않아도 되었다. 이렇게 수정을 하니 이미 빌드된 외부 라이브러리를 아무런 문제 없이 재사용 할 수 있게 되었다.
  2. 롤리팝 이후에 AOSP 빌드 시스템의 경우 LOCAL_CFLAGS_x86_64, LOCAL_SRC_FILES_x86_64 같은 CPU arch 별 LOCAL 변수를 사용할 수 있다. ndk-build 스크립트도 조금 수정해서 arch별 LOCAL_* 변수를 사용할 수 있도록 하였고 잘 동작 하였다.
  3. AOSP 빌드 시스템의 경우 external/lame 외부 모듈에서 mm 명령을 실행하면 최상위 빌드 디렉토리로 옮겨간 후에 make -C $TOPDIR build/core/main.mk lame 식으로 외부 모듈을 컴파일 하고, 모든 INCLUDE 경로를 TOPDIR 상대 경로로 인식을 하게 된다. 그러나 ndk-build의 경우 make -C $TOPDIR이 아닌 make -C external/module_name 식으로 실행이 되어 모든 LOCAL_C_INCLUDES 경로가 현재 프로젝트 경로에 대한 상대 경로로 인식된다. ndk-build도 AOSP처럼 TOPDIR에 대한 상대경로를 인식하게끔 -C $TOPDIR 방식을 적용하게 스크립트를 고쳐봤는데 큰 문제 없이 잘 빌드 되었다. (단 이 경우 ndk-build 실행 스크립트를 조금 수정해야 했으며, Android.mk 및 Application.mk, NDK_PROJECT_PATH 경로를 직접 지정해 주어야 했다.)

이정도 수정을 하니 ndk-build 시스템을 AOSP 빌드시스템과 마찬가지로 외부 라이브러리 의존성에 대해서 덜 걱정하도록 해 주었고 쓸만해졌다.

이렇게 수정을 한 ndk-build를 통해서 arm64용으로 FFmpeg + lame + 기타 jni 소스등을 빌드해서 테스트하여 잘 실행이 됨을 확인하였다.

(※주의: arm64와 같이 64비트의 경우는 AOSP 롤리팝 이후부터 지원됨)

text relocations 문제

안드로이드 6.0 이후부터는 text relocations 문제가 있는 shared 라이브러리를 사용하는 경우 앱이 실행이 되지 않는다. arm64/x86_64에서는 이 문제가 없거나 해결하기 쉬운 편이나, 유독 x86 아키텍쳐에서는 text relocations 문제를 쉽게 해결하기 어려우며 이러한 문제가 발생하게 된다.

앱이 타겟 API가 22 이하로 빌드된 경우에는 경고만 뿌리고 실행이 정상적으로 되지만, 클론 리플레이어의 경우는 타겟 API가 23이며 text relocations 오류를 내며 앱이 실행조차 되지 않는다. 그래서 원래는 x86 아키텍쳐용으로 FFmpeg/mpg123 등등을 컴파일하고 지원하려고 했지만 text relocations 문제로 인해 x86을 당장 지원하기는 어렵게 되었고 x86_64 및 arm64 지원용 라이브러리를 우선적으로 테스트하게 되었던 것이다.

Text relocation 문제가 있는 라이브러리는 다음과 같았다.

  1. FFmpeg의 libavcodec / libswresample / libswscale(클론 리플레이어는 사용 안함) (libavutil, libavformat은 정상) https://trac.ffmpeg.org/ticket/4928 참고
  2. mpg123
  3. 기타 클론 리플레이어와는 무관하지만 구글링을 해보면 x264 등등의 라이브러리도 text relocation 문제가 있었으며 shared 라이브러리로 빌드시에 text relocation 경고를 보여주었다. (x264의 경우 구 버전은 text relocation 문제가 없었다가 AMD64 + i386 어셈블리어를 통합시키는 과정에서 text relocation 문제가 발생하는 것을 우연히 발견하였다. x264 개발자는 text relocation문제를 수정할 계획이 없다고 밝혔지만 정작 원래는 문제 없던 것이 새롭게 문제가 생겼던 것...) TEXTREL 문제는 readelf -d 명령을 통해 확인할 수 있다. (readelf -d 라이브러파일경로 | grep TEXTREL)
  4. 문제가 생기는 부분은 어셈블러로 최적화된 부분이다. FFmpeg의 경우 컴파일 옵션으로 YASM 어셈블러를 사용하지 않게 처리하면 TEXTRELs 문제가 발생하지 않는다. 다만 이 경우 성능저하가 있게 된다.


by dumpcookie 2017. 3. 1. 19:01
| 1 2 3 4 ··· 7 |