최근의 일련의 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

앱이 실행이 안된다는 보고가 있어서 이 문제를 살펴보았더니 인텔 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 |