백준저지에서는 다양한 언어를 지원합니다. 그 중에서도 32bit Assembly 언어도 지원을 합니다. 32bit Assembly로 백준저지에서 간단한 문제를 풀어보도록 하겠습니다.


일단 로컬에서 Assebmly 코딩을 하기 위해서 어셈블 환경을 만들어 볼 것인데요, 일단 Ubuntu 16.04기준으로 하겠습니다.


nasm과 gcc를 설치해줍니다. 만약 호스트가 64bit 운영체제라면 32bit 컴파일을 위해서 gcc-multilib을 설치해줘야 합니다.


다음 명령어로 어셈블리를 ELF파일로 빌드할 수 있습니다.


nasm -f elf32 assembly.asm -o assembly.o gcc -m32 assembly.o -o assembly.out


어셈블러로 어셈블리 소스코드를 오브젝트 파일로 바꾼 뒤, 실행가능한 ELF 바이너리로 빌드하는 과정입니다.


https://www.acmicpc.net/problem/2557


백준 2557번 Hello World를 어셈블리 코드로 작성해보도록 하겠습니다.


printf 라이브러리 콜을 이용해서 작성한 코드입니다.


extern을 이용해서 사용할 함수 명을 가지고 오며, 함수의 인자를 스택에다가 push해서 호출하는 것을 확인할 수 있습니다. Calling convention에 따라서 Caller function에서 스택을 정리하는 모습(add esp, 4)도 보입니다.

 
extern printf
section .data
    msg : db "Hello World!", 0xa, 0
        
section .text
    global main
main:
    push msg
    call printf
    add esp, 4
ret



write 시스템 콜을 이용해서 작성한 코드입니다.

int명령어로 인터럽트를 발생해서 시스템 콜을 호출하며, 시스템 콜 인자를 지정된 레지스터에 넣어서 호출하는 것을 확인할 수 있습니다.

EAX에는 시스템 콜 번호(write의 시스템 콜 번호는 4입니다), EBX는 첫번째 인자(stdout=1), ECX는 write할 Address로 두번째 인자, EDX는 세번째 인자로 문자열의 길이가 들어가게 되었습니다.

 
M: db "Hello World!"
section .text
global main
main:
    mov edx, 12
    mov ecx, M
    mov ebx, 1
    mov eax, 4
    int 0x80
    ret


어셈블리로 ps 문제 등을 풀면 어셈블리 언어에 대한 이해도가 늘고 리버싱이 쉬워질 것 같습니다.

한국정보올림피아드(정올) 2017년도 초등부 문제 풀이 포스팅입니다.


문제들은 백준저지에서 참고하고 풀이했습니다.

https://www.acmicpc.net/category/detail/1775


총 4문제로 이루어져 있는데, 제가 풀 때 당시에는 백준저지에는 3문제만 올라와있어서 3문제에 대한 코드만 올립니다.


https://www.acmicpc.net/problem/14697


2번, 방 배정하기 문제입니다. (백준저지 14697)


N값이 작으므로 3중 포문으로 O(N^3)으로 완전탐색으로 Naive하게 풀었습니다.


#include <bits/stdc++.h>
using namespace std;

int main() {
    int a, b, c, total;
    cin >> a >> b >> c >> total;
    
    int a_lim = total/a;
    int b_lim = total/b;
    int c_lim = total/c;
    for (int i=0; i <= c_lim; i++) {
        int cur = c * i;
        for (int j=0; j <= b_lim; j++) {
            int cur2 = cur + j*b;
            for (int k=0; k <= a_lim; k++) {
                int cur3 = cur2 + k*a;
                if (cur3 == total) {
                    cout << 1 << '\n';
                    return 0;
                }
            }
        }
    }
    cout << 0 << '\n';
	return 0;
}


https://www.acmicpc.net/problem/14863

3번, 서울에서 경산까지 문제입니다. (백준저지 14863)


DFS를 통한 완전탐색으로 풀되, 중복되어 풀리는 문제들(Overlapping-subproblems)는 Memoization 패턴을 이용하여 중복해서 풀리지 않도록 처리했습니다. 그리고 마지막 구간까지 도달하지 못하는 경우에 대해서 제외하도록 처리해야하는 것이 좀 까다로웠습니다.



#include <bits/stdc++.h>
using namespace std;

#define IMPOSSIBLE -987654321
typedef long long ll;
int dp[101][100001]; //[range][minute left]
int walk_min[101];
int walk_get[101];
int bicy_min[101];
int bicy_get[101];
int n, k;
int get(int segment, int minute_left) {
    if (segment >= n) return 0;
    if (minute_left <= 0) return IMPOSSIBLE;
    int& ret = dp[segment][minute_left];
    if (ret != -1) {
        return ret;
    };
    
    ret = IMPOSSIBLE;
    if (minute_left >= walk_min[segment] and get(segment+1, minute_left - walk_min[segment]) != IMPOSSIBLE) {
        ret = max(ret, get(segment+1, minute_left - walk_min[segment]) + walk_get[segment]);
    }
    if (minute_left >= bicy_min[segment] and get(segment+1, minute_left - bicy_min[segment]) != IMPOSSIBLE) {
        ret = max(ret, get(segment+1, minute_left - bicy_min[segment]) + bicy_get[segment]);
    }
    return ret;
    
}

int main() {

    scanf("%d %d", &n, &k);
    for (int i=0; i < n; i++) {
        scanf("%d %d %d %d", &walk_min[i], &walk_get[i], &bicy_min[i], &bicy_get[i]);    
    }
    memset(dp, -1, sizeof(dp));
    
    cout << get(0, k) << '\n';
	return 0;
}


https://www.acmicpc.net/problem/14864

4번, 줄서기 문제입니다. (백준저지 14864)


개인적으로 난이도가 꽤나 있었고, 완전 Nice한 해답은 아니라고 생각합니다.

일단 순서쌍에서 알아낼 수 있는 정보는 다음과 같습니다.

1. 나보다 뒤에 있는 놈에 대해서, 내가 그 놈보다 큰지 아닌지를 확인할 수 있다.

2. 나보다 앞에 있는 놈 중에 나보다 큰 놈이 몇 개인지 알 수 있다.    


물론 나보다 앞에 있는 놈 중에 큰놈이 몇개인지 뿐만 아니라 누군지까지 확인할 수 있지만, 일단은 저 두개를 이용해서 문제를 풀어보았습니다.

맨 뒤에서 부터 채워가면서 숫자를 넣을 것인데, 일단 집합 S를 다음과 같이 초기화 합니다. S = {1, 2, 3, ... , N}

맨 뒤에있는 숫자 입장에서는 나머지 모두가 나보다 앞에 있으므로, 나머지 수 중 자신보다 큰 수가 몇개인지 알 수 있습니다.

그러면 앞에있는 수 중 자신보다 큰 수의 개수를 k라고 하면, 집합 S에서 k번째 큰 수를 빼서 자신의 값에 집어넣습니다.


이런식으로 맨 앞까지 진행합니다.


이러면 일단 조건을 만족할만한 정답 셋을 하나 구할 수 있는데, Valid한 정답을 갖는다면 Unique한 정답을 가질 것입니다.

하지만 해당 인풋이 Invalid할 수도 있으니 한번 체크를 해야 합니다. (Invalid한 정답이라면 -1를 출력해야함)

이때 Strict하게 체크를 하려면 자신보다 뒤에 있는 숫자 중, 순서쌍에 해당된다면 자신보다 작은지, 순서쌍에 해당되지 않는다면 자신보다 큰지를 확인해야하는데, 둘다 체크하게 되면 TLE가 날 것입니다.   

특히나 순서쌍애 해당되지 않는 애들까지 체크를 하게 되면 O(N^2)이므로 꼼짝없이 TLE다. 하지만 순서쌍 개수는 100만개이므로, 순서쌍에 해당되는 경우만 체크할 경우 TLE는 안날 수 있습니다. 따라서 순서쌍에 해당되는 경우만 자신보다 작은 값을 갖는지만 체크하면 됩니다.


이 부분의 경우 -1이 정답인데 아니게 나올까봐 조마조마했는데 정답 판정이 났다. 아마 앞에서 만든 Answer Array가 주어진 순서쌍 조건이 맞는지만 확인해도빠진 조건이 있는 반례가 없다는게 논리적으로 맞아서 그런 것인지는 잘 모르겠습니다.

#include <bits/stdc++.h>
using namespace std;

#define NUM_STUDENT 100001 //학생수 최대
#define UNDEFINED 0 // 정답이 결정되지 않은 상태
set<int> larger_than_me[NUM_STUDENT], smaller_than_me[NUM_STUDENT];

int ans[NUM_STUDENT];
int n, m;
set<int> rest;
void fail() {
    cout << -1 << '\n';
    exit(EXIT_SUCCESS);
}
int main() {
    scanf("%d%d", &n, &m);
    
    //initialize rest set
    for (int i=1; i<=n;i++) {
        rest.insert(-i);
    }
    //initialize rest set
    
    if (n == 1) { //학생수 1명일때만 예외처리
        cout << 1 << '\n';
        return 0;
    }
    
    for (int i=0; i < m; i++) {
        int a, b;
        scanf("%d %d", &a, &b); // a > b라는 뜻
        smaller_than_me[a].insert(b); // a보다 작은 set에 b 추가.
        larger_than_me[b].insert(a);// b보다 큰 set에 a추가.
    }
    
    for (int cur = n; cur >= 1; cur--) {
        int larger_num = larger_than_me[cur].size();
        auto it = rest.begin();
        for (int i=0; i < larger_num; i++) {
            it++;
        }
        int kth_largest = -*it;
        ans[cur] = kth_largest;
        rest.erase(it);    
    }
    
    //check for validation - 주어진 조건은 맞는지 확인. 빠진 조건이 있는지는 안확인 ㅎㅎㅎ
    for (int cur = 1; cur < n; cur++) {
        int my_val = ans[cur];
        for (auto it : smaller_than_me[cur]) {
            int smaller_val = ans[it];
            if (smaller_val > my_val) {
                fail();
            }
        }
    }
    
    for (int i=1; i <= n; i++) {
        cout << ans[i] << ' ';
    }
    cout << '\n';
	return 0;
}


GDB는 일반 어플리케이션 디버거로서도 사용되지만, 특히나 시스템 해킹을 공부하는 사람들이 많이 사용하곤 한다. 자주 사용하는 명령어들을 한번 정리해보겠다.


1. 실행

바이너리 실행 및 디버거 Attach

gdb -q [파일명]


런타임 Attach

gdb -q -p [pid]


2. 메모리 검사(Examine/ x 명령어)


x/[개수][포맷][크기] [주소]


개수에는 숫자가 들어간다


포맷은 다음의 경우가 있다.

1) x - 16진수

2) d - 10진수

3) c - 문자

4) s - 스트링

5) i - 인스트럭션(어셈블리 명령어)


크기는 다음의 경우가 있다.

1) b - 바이트(1byte)

2) h - 하프워드(2byte)

3) w - 워드(4byte)

4) g - 자이언트(8byte)


주소는 다음과 같이 있다.

1) 0xabfdefg - 16진수로 주소값을 직접 사용

2) $eip - 레지스터가 가지고 있는 값의 메모리 주소를 검사. 레지스터 이름 앞에 $를 붙여야 한다.



3. 디버기(debugee) 프로세스의 pid 및 정보 알아내기

(gdb) info inferior


4. 브레이크 포인트 걸기


(gdb) b *0x808080 
(gdb) b *main 
(gdb) b *main+5


와 같이 다양하게 브레이크 포인트를 걸 수 있다.


5. 브레이크 포인트 걸린 위치 확인


(gdb) info b


6. 프로세스 실행

(gdb) run

혹은

(gdb) r


7. 다음 브레이크 포인트까지 계속 실행

(gdb) continue

줄여서

(gdb) cont


8. 한 인스트럭션만 실행

(gdb) next instruction

(gdb) ni


-----------------

syscall들

https://syscalls.kernelgrok.com/



+ Recent posts