검색결과 리스트
2017/03/01에 해당되는 글 2건
- 2017.03.01 클론 리플레이어 x86_64/arm64 지원
- 2017.03.01 x86 어셈블리어 배우기 - 리버스엔지니어링 기초
앱이 실행이 안된다는 보고가 있어서 이 문제를 살펴보았더니 인텔 CPU용 안드로이드 x86/x86_64에서 실행되지 않는 문제였다. 리포팅된 로그에 남아있는 오류는 대략 다음과 같았다.
java.lang.UnsatisfiedLinkError: dlopen failed: \ "/data/app-lib/org.dumpcookie.ringdroidclone-1/libffmpeg_jni.so" has unexpected e_machine: 40
특히 위 링크의 마지막 문서를 보면 1997년에 만들어진 Miller의 Recursive Make Considered Harmful 이라는 make에 대한 흥미로운 문서를 만날 수 있다. (2002년 2008년 개정판(?)을 pdf로 볼 수 있다)
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을 보다 손쉽게 할 수 있는 방법이 있다. 공식 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 역시 컴파일이 잘 된다.
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에 올려두었다.
위에서 기술한 방식으로 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 스크립트의 다음과 같은 내용을 수정하였다.
이정도 수정을 하니 ndk-build 시스템을 AOSP 빌드시스템과 마찬가지로 외부 라이브러리 의존성에 대해서 덜 걱정하도록 해 주었고 쓸만해졌다.
이렇게 수정을 한 ndk-build를 통해서 arm64용으로 FFmpeg + lame + 기타 jni 소스등을 빌드해서 테스트하여 잘 실행이 됨을 확인하였다.
(※주의: arm64와 같이 64비트의 경우는 AOSP 롤리팝 이후부터 지원됨)
안드로이드 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 문제가 있는 라이브러리는 다음과 같았다.
클론리플레이어 오디오 인코딩 변환 지원 (0) | 2015.04.24 |
---|---|
구간반복 학습앱 소개 (20) | 2014.11.02 |
ringdroid 클론 반복 학습기 (0) | 2014.09.12 |
오랫동안 프로그래밍을 해본 사용자라 할지라도 어셈블리어가 필요하거나 리버스엔지니어링(역공학)이 필요한 경우는 별로 많지 않다. 본인도 90년대에 처음 GW 베이직을 시작으로 터보파스칼/터보C를 사용하며 프로그래밍의 기초를 배웠던 것이 그 시작이었지만 어셈블리어를 사용할 기회는 그다지 많지 않았다. 터보 파스칼의 그래픽 라이브러리를 사용하다가 마이크로소프트웨어 잡지책에 나와있던 원그리기/직선 그리기 알고리즘을 MASM 어셈블리어로 구현하고 이를 개선하려고 하면서 VGA 메모리 접근을 위해 주소를 어셈블리어로 직접 다루던 부분을 디버깅하던 기억이 아직도 남아있다. 당시 어셈블리어는 이해하기 어려웠고 고치기도 힘들었으나 최적화되고 나서 기대했던 속도가 나왔을 당시의 느꼈던 감격은 어셈블러의 단점을 보상하고도 남음이 있었다.
아무튼 지금 이 글을 쓰고있는 시점에서 조차도 본인은 어셈블리어에 대한 경험이 별로 없지만, 어셈블리어 혹은 리버스엔지니어링에 대한 기초적인 입문서도 많지 않다는 것에 조금 의아스럽게 생각하며, 어셈블리어에 완전히 생초짜라도 누구나 쉽게 따라해보면서 어셈블리어의 기초와 리버스엔지니어링을 같이 배워가며 문서를 쓸 예정이다.
C언어 기초 및 리눅스/유닉스에 어느정도 익숙한 분들을 대상으로 하지만 내용은 훨씬 쉽게 쓰려고 한다.
가장 먼저 초간단 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 명령과 똑같다.
이렇게 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
그러면 이제 조금 더 그럴듯한 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 -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 -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/
기타 케이블의 커패시턴스 효과 (4) | 2022.07.11 |
---|---|
인라인 어셈블리 기초 - x86 어셈블리어 예제를 통해서 배우기 (0) | 2017.03.05 |
아멘 샬롬 (0) | 2015.04.21 |
아가페 프로토스 (1) | 2015.04.20 |
RECENT COMMENT