프로그래밍에서 시간을 표현하거나 계산 할 때 년/월/일/시/분/초를 나누어 표현 또는 계산하는 방법도 있으나,

Timestamp를 사용하는 경우도 자주 있다.


Timestamp란 특정 시간을 기준으로 지금까지 경과된 시간을 나타내는 숫자이다.


년/월/일/시/분/초 형식으로 구분하여 사용하면 인간이 읽기에는 편하나 더하고 빼고 등의 시간 계산을 하기에는 복잡하고 비효율적이다.

그래서 이러한 경우에는 Timestamp를 자주 사용한다.


Timestamp를 통신에 사용할 경우 주의해야 할 점이 있는데 OS 또는 언어마다 Timestamp의 기준시와 단위가 달라진다는 점이다.


OS 별 Timestamp는 다음과 같다.


<Unix>

 time_t

 UTC 1970년 1월 1일 0시 0분 0초를 기준으로 하는 초 단위의 시간을 저장하는 형식

https://ko.wikipedia.org/wiki/Time.h

2038년 문제를 주의해야 한다. 32bit 자료형을 사용할 경우 2038년 1월 19일까지 밖에 표현하지 못한다.


<Windows>

FILETIME

Contains a 64-bit value representing the number of 100-nanosecond intervals

since January 1, 1601 (UTC).

https://msdn.microsoft.com/ko-kr/library/windows/desktop/ms724284(v=vs.85).aspx



Windows에서 사용하는 Timestamp는 1601년 1월 1일 0시부터 지금까지의 시간을 100 나노초 단위로 표시한다.

반면 Unix에서는 1970년 1월 1일 0시부터 지금까지 경과된 시간을 초 단위로 표시한다.

Python은 Unix와 동일한 Timestamp를 사용한다.


그렇기 때문에 통신에서 Timestamp를 사용할 경우에는 OS에 따라 변환 과정을 거쳐야 한다.


변환 방법은 1)단위를 통일하고 2)기준시간을 변경하면 된다.



예를 들어 현재 시점으로 만들어진 Timestamp를 변환한다고 가정해 보자.

작업 환경이 Windows 라면 의 크기가 Timestamp가 될 것이고 Unix 라면 가 될 것이다.


로 변환하기 위해서는 우선 단위를 '100나노초'에서 '초'로 변경하고 를 빼면 된다.


의 크기는 116444736000000000 나노초 또는 11644473600 초 이다.

그리고 나노초를 초로 변경하기 위해서는 10000000을 곱하면 된다.


WinAPI를 기준으로 아래와 같이 구현할 수 있다.


1
2
3
4
5
6
7
8
9
LARGE_INTEGER Win2UnixStamp (LARGE_INTEGER largeWinStamp)
{
    LARGE_INTEGER largeUnixStamp;
    LARGE_INTEGER largeUnixInitValue;
    largeUnixInitValue.QuadPart = 11644473600// UNIX time_t 1970/01/01 00:00 (UTF)
    largeUnixStamp.QuadPart = (largeWinStamp.QuadPart / 10000000- largeUnixInitValue.QuadPart;
    return largeUnixStamp;
 
}
cs


1
2
3
4
5
6
7
8
LARGE_INTEGER Unix2WinStamp (LARGE_INTEGER largeUnixStamp)
{
    LARGE_INTEGER largeWinStamp;
    LARGE_INTEGER largeUnixInitValue;
    largeUnixInitValue.QuadPart = 116444736000000000;
    largeWinStamp.QuadPart = (largeUnixStamp.QuadPart * 10000000+ largeUnixInitValue.QuadPart;
    return largeWinStamp;
}
cs


이전 포스트에서 rand() 함수의 취약점을 설명하였다.


rand() 함수를 보완한 랜덤 함수가 CryptGenRandom() 함수이다.


CryptGenRandom 함수는 crpytographic service provider(CSP)를 이용한 랜덤 함수로 암호학적으로 랜덤하게 난수값을 생성한다. 다만, rand() 함수에 비해 매우 느리다는 단점이 있다.


사용 방법은 아래와 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include "stdafx.h"
#include <Windows.h>
#include <STDIO.H>
#include <Wincrypt.h>
 
#define MAX_RANDOM_NUM 100
 
int main(int argc, char* argv[])
{
    //--------------------------------------------------------------------
    // 변수를 선언하고 초기화 합니다.
    HCRYPTPROV   hCryptProv;
    BYTE         pbData;
 
    //-------------------------------------------------------------------
    // 암호 제공자의 컨텍스트 핸들을 얻습니다.
    if (CryptAcquireContext(&hCryptProv, NULLNULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
    {
        printf("New CryptAcquireContext succeeded. \n");
    }
    else
    {
        printf("Error 0x%x during CryptAcquireContext!\n", GetLastError());
        goto ERR;
    }
 
    //--------------------------------------------------------------------
    // BYTE 범위내에서 난수를 생성합니다.
    if(CryptGenRandom(hCryptProv, 1&pbData)) 
    {
        printf("Random number is: %d.\n", ((int)pbData) * MAX_RANDOM_NUM / 255);
    }
    else
    {
        printf("Error 0x%x during CryptGenRandom.\n", GetLastError());
        goto ERR;
    }
 
ERR:
    //-------------------------------------------------------------------
    // 컨텍스트 핸들을 해제합니다.
    if(hCryptProv)
    {
        if (!CryptReleaseContext(hCryptProv, 0))
        {
            printf("Failed CryptReleaseContext\n");
        }
    }
 
    return 0;
}
cs



CryptGetRandom() 함수는 현재 deprecated 되었다.

Cryptography Next Generation APIs를 사용해라.


<본 글은 win32 api를 바탕으로 작성되었습니다.>


개발을 하다 보면 랜덤 값을 생성해야 하는 경우가 있다. 이때 자주 사용되는 함수가 rand() 함수이다.



* rand() 함수 설명


C에서 rand() 함수는 랜덤 값을 생성하는 함수이다.

rand() 함수 사용법은 아래와 같다.


1
2
3
4
5
6
int i;
unsigned int nSeed = 1000;
 
srand(nSeed);
for( i = 0; i < 10; i++ )
    printf("%d ", rand() );
cs


seed 값을 설정하고 rand() 함수를 호출하면 0~0x7fff 사이의 값이 랜덤으로 리턴된다.

srand() 함수에 seed 값을 주면 전달 된 seed 값을 기준으로 정해진 알고리즘에 따라 0~0x7fff 사이의 랜덤 값 리스트를 생성하게 된다.

그 후 rand() 함수를 호출하면 랜덤 값 리스트에서 값을 순서대로 하나씩 꺼내 리턴한다.



* rand() 함수의 한계


결론부터 말하자면 rand() 함수는 보안에 취약하므로 사용하지 마라.

위에서 설명했듯이 rand() 함수는 srand() 함수를 통해 생성된 리스트에서 값을 하나씩 꺼내게 되고,

srand() 함수는 전달 된 seed 값을 기준으로 정해진 알고리즘에 따라 리스트를 생성한다.

즉, seed 값을 알게 되면 rand() 함수로부터 리턴 될 값을 예측할 수 있게 된다.


아래 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void rand_test_1()
{
    int i;
    unsigned int nSeed = 1000;
 
    srand(nSeed);
    for( i = 0; i < 10; i++ )
        printf("%d ", rand() );
 
    printf("\n");
}
 
int main(int argc, char* argv[])
{
    rand_test_1();
    Sleep(1000);
    rand_test_1();
    Sleep(1000);
    rand_test_1();
 
    return 0;
}
cs


결과는 다음과 같다.

1
2
3
4
3304 8221 26849 14038 1509 6367 7856 21362 6968 10160
3304 8221 26849 14038 1509 6367 7856 21362 6968 10160
3304 8221 26849 14038 1509 6367 7856 21362 6968 10160
Press any key to continue
cs


seed 값을 알게 되면 그 다음 생성되는 랜덤 값은 항상 같게 되고 이를 유추할 수 있게 된다.

그래서 seed 값을 추측할 수 없게 하기 위해 seed를 현재 시간으로 하기도 한다.


아래 코드는 time() 함수를 사용하여 seed 값에 현재시간 정보를 넣은 코드이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void rand_test_2()
{
    int i;
    unsigned int nSeed = (unsigned)time( NULL );
 
    srand(nSeed);
    for( i = 0; i < 10; i++ )
        printf("%d ", rand() );
 
    printf("\n");
}
 
int main(int argc, char* argv[])
{
    rand_test_2();
    Sleep(1000);
    rand_test_2();
    Sleep(1000);
    rand_test_2();
 
    return 0;
}
cs


결과는 아래와 같다.

1
2
3
4
22734 32447 12644 11773 13510 15387 24256 27097 22692 24687
25993 11452 15295 8016 18303 2103 17522 22028 18955 16531
29304 31360 8861 28825 24290 18879 27819 2892 17260 8311
Press any key to continue
cs


이렇게 하면 seed 값을 유추하기 어려우나 이는 어려운 것일 뿐 불가능한 것은 아니다.


암호학적으로 랜덤한 값을 만들기 위해 Unix 계열에서는 '/dev/random' 또는 '/dev/urandom'를,

Windows 계열에서는 'CryptGenRandom()' 함수를 사용하는 것이 좋다. (CryptGetRandom() 함수는 deprecated 되었다. Cryptography Next Generation APIs를 사용해라)

'프로그래밍 > C' 카테고리의 다른 글

[C/C++] CryptGenRandom()를 이용한 랜덤값 출력  (0) 2018.02.09


1
2
3
4
5
6
7
8
9
>>> def f (x, y=[]) :
    y.append(x)
    print(y)
>>> def test () :
    f('a')
    f('a')
    f('a')
>>> test()

cs


9번째 라인의 출력 결과는 어떻게 될까?

위 코드의 결과는 아래와 같다.


1
2
3
4
>>> test()
['a']
['a''a']
['a''a''a']
cs


나는 당연히 ['a']가 세 번 출력될 것이라 예상했는데 이전 값들이 계속 남아 출력되어 적잖이 당황했다.

정황으로 보아 f 함수의 y 변수가 지워지지 않고 계속 남아있는 것으로 보이는데 이해가 되지 않았다.

y 변수는 분명 지역변수로 매번 생성 될텐데 어째서 지워지지 않았을까.


이러한 내용과 관련하여 검색을 해보고 아래와 같은 사실을 알게 되었다.

Default Parameter Values in Python 참고


python에서 함수 인자의 기본값으로 "mutable" 성격의 오브젝트가 선언되고 그 기본값을 통해 변수가 생성될 경우 해당 변수는 static 변수의 성격을 갖게 된다.


일부 사이트에서는 인자의 기본값이 mutable 이면 global 변수가 된다고 하는데,

변수의 life time은 global 이지만 scope이 함수 내부여서 global 변수 보다는 static 변수로 보는 것이 맞을 것 같다.


이러한 이유 때문에 동일한 함수(위 코드에서 f 함수)를 계속 호출하면 매번 y 리스트가 새로 생성되는 것이 아니라, 이전에 호출 되었던 y 변수를 다시 불러와 다시 사용하게 되는 것이다.


리스트, 딕셔너리 뿐만 아니라 클래스와 같은 오브젝트들도 함수 인자의 기본 값으로 사용하게 될 경우 위의 내용과 동일하게 동작한다.


이러한 문제를 해결하기 위한 방법은 여러가지가 있을 것인데 아래 코드와 같이 바꿔서 사용할 수도 있다.


1
2
3
4
5
6
>>> def f (x, y=None) :
    if y is None:
        y=[]
    y.append(x)
    print(y)
 

cs


삽질을 피하기 위해 인자의 기본 값 설정 시에는 가능하면 "mutable" 한 오브젝트는 사용하지 말자.

지금껏 개발을 해오면서 ASCII와 ANSI의 차이에 대해 깊게 생각해 본 적이 없었다.

UTF-8 기본으로 하여 개발을 해왔던 이유도 있거니와

ASCII=ANSI로 생각해도 사실 큰 문제는 없어왔다.


점 하나 그냥 찍어서는 안되는 개발에서 이렇게 기본기가 부족하니

내 실력이 이정도 뿐인 것 아니겠는가.


약간의 짬을 내어 인코딩 관련하여 정리를 해보려고 한다.




ASCII / ANSI / EUC-KR / CP949 / UTF-8 / UNICODE



1. ASCII(American Standard Code for Information Interchange)

ASCII는 최초의 문자열 인코딩이다.

7 bit로 구성되어 있으며, 영어를 위한 문자, 숫자, 특수문자, 기호 등 128개 문자를 표현할 수 있다.


여기서 주목해야 하는 것이 바로 '영어를 위한 문자'이다. (ASCII의 A가 'American'인 점을 주목)

ASCII에서는 영어만을 고려하여 만들어졌고, 일본어 중국어 등 다른 언어는 표현이 불가능하다.


이후 다른 언어를 지원해야 할 필요가 생겨 만들어진 인코딩이 ANSI이다.

ASCII 문자표는 여기서 확인 가능하다.



2. ANSI(American National Standard Institute)

ANSI는 8bit로 구성되어 있으며 256개의 문자를 표현할 수 있다.

ANSI는 ASCII의 확장판으로 이해하면 된다. 

그 이유는 ASCII에서 1bit를 더 사용한 것이기 때문이다.

ANSI의 앞 7bit는 ASCII와 동일하고, 뒤에 1bit를 이용하여 다른 언어의 문자를 표현한다.


그런데 새로 추가 된 128개 문자로는 모든 언어의 문자를 표현할 수 없다.

그래서 생긴 개념이 CodePage 이다.


각 언어별로 Code 값을 주고, Code마다 다른 문자열 표를 의미하도록 약속을 했다.

쉽게 생각하면 아래와 같이 설명할 수 있겠다.


 ANSI = ASCII(7bit) + CodePage(1bit)


이러한 원리를 고려하면 다음과 같이 정리할 수 있다.


첫째, 영어만 사용하거나 ASCII를 사용할 경우 세계 어디에서나 사용에 문제가 없다.

둘째, 영어 외 다른 언어를 사용할 경우 ANSI는 Code Page를 동일하게 맞춰야 한다.

Code Page가 다를 경우 의도와 다른 결과가 나올 수 있다.



3. EUC-KR(Extended Unix Code-Korea)

EUC-KR은 한글 지원을 위해 유닉스 계열에서 나온 완성형 코드 조합이다.

완성형 코드란 완성 된 문자 하나하나마다 코드 번호를 부여한 것이다. 

반대되는 개념으로 조합형 코드가 있는데, 이는 한글의 자음과 모음 각각에 코드 번호를 부여한 후 초성, 중성, 종성을 조합하여 하나의 문자를 나타내는 방식을 말한다.

EUC-KR은 ANSI를 한국에서 확장한 것으로 외국에서는 지원이 안 될 가능성이 높다.



4. CP949(Code Page 949)

CP949는 한글 지원을 위해 윈도우즈 계열에서 나온 확장 완성형 코드 조합이다.

EUC-KR은 2bytes의 완성형 코드로 2bytes 내에서는 표현할 수 있는 완성된 문자의 수는 한계가 있었다.

그래서 마이크로소프트에서 EUC-KR을 개선, 확장하여 만든 것이 CP949 이다. 여기서 949는 페이지 번호를 의미하며 한국을 의미한다. (참고로 일본어는 CP932, 중국어 간체는 CP936이다.)

기본적으로 EUC-KR과 호환이 되며, EUC-KR에서 표현이 되지 않는 문자는 조합을 하여 표현한다.

마이크로소프트가 만들었다고 하여 MS949라고 부르기도 한다.



5. UTF-8(Universal Coded Character Set + Transformation Format – 8-bit)

UTF-8은 유니코드를 위한 가변 길이 문자 인코딩(멀티바이트) 방식 중 하나로, ANSI의 단점을 보완하기 위해 만들어졌다.

ANSI는 다국어를 지원하기 위해 CodePage 정보를 미리 알고 있어야 한다. UTF-8은 멀티바이트 개념을 사용하여 하나의 Character Set에 거의 모든 문자를 넣었다.


멀티바이트란 표현해야 하는 문자에 따라 글자 크기를 가변으로 변경하여 사용하는 것을 말한다.

ANSI는 고정바이트(1byte) 형태로 최대 256자 까지만 표현이 가능하나 UTF-8은 멀티바이트(1~4bytes)로 최대 1,112,064자 까지 표현이 가능하다.

첫 128자는 ASCII 코드 값으로 ANSI와 UTF-8이 동일하다. 그래서 영어를 사용할 경우 1byte만 사용한다.

2bytes를 사용하며, 중동지역 언어 또는 많은 유럽 언어가 여기에 속한다. 한국,중국,일본 등 동아시아권 언어는 3bytes 이상을 사용한다.

UTF-8은 매우 일반적인 인코딩 방식이지만 3bytes 이상의 문자를 사용할 경우에는 비효율적일 수 있다.


UTF-16은 16bit 기반으로 저장하는 UTF-8의 변형이라고 보면 된다.

한글의 경우 UTF-8로 저장할 경우 3bytes가 필요한데, UTF-16으로 저장하면 2bytes면 되어 용량의 이점이 있다고 한다.

그러나 경우에 따라서는 2bytes 이상을 사용할 경우가 있어 용량의 이점이 크다고 보긴 어렵고, 

엔디안 처리를 고려함에 따른 복잡성 증대나 ANSI와 호환이 안되는 단점이 있다.


UTF-32는 모든 문자를 4bytes로 인코딩한다. 문자 변환 알고리즘이나 가변길이 인코딩 방식에 대한 고민을 하고 싶지 않을 때 유용할 수 있다. 그러나 매우 비효율적으로 메모리를 사용하므로 자주 사용되지는 않는다.



UTF-8, UTF-16 모두 유니코드를 지원하기 위한 인코딩 방식이다. 이들을 이해하기 위해서는 유니코드에 대한 이해가 필요하다. 일반적으로 UTF-8과 유니코드를 동일하게 생각하는데 약간의 차이가 있다.



6. UNICODE

UNICODE는 전 세계의 모든 문자를 컴퓨터에서 일관되게 표현할 수 있도록 고안된 코드 조합이다.

여기서 주의해야 할 것이 유니코드는 '인코딩'이 아니라는 것이다. 유니코드는 전세계 거의 모든 문자를

2bytes 숫자로 1:1 매핑 시키는 '방식'을 말하고, 유니코드를 표현하는 여러가지 '인코딩' 방식들이 존재하는 것이다.

UTF-8, UTF-16 등이 그 인코딩 중 하나인 것들이다.


유니코드_목록을 통해 매핑 테이블을 확인할 수 있다.

한글은 한글_목록을 보면 된다.


여기서 '가'를 찾아보면 유니코드 값이 'AC00' 인 것을 알 수 있다. 16진수 AC00은 10진수로 44,032 인데 8bit로 나누기에 너무 크다. 이 값을 8bit 단위로 쪼개어 저장하는 방법이 UTF-8이다. (쪼개는 방법은 여기를 참조하자.)

윈도우 비스타 이상에서는 유니코드를 UTF-16으로 인코딩한다.


유니코드 값을 바로 사용하지 않고 UTF-8을 이용하여 사용하다보니 UTF-8=UNICODE 라 생각하기 쉬운데 별 생각 없이 유니코드 목록에서 값을 찾게 되면 맨붕에 빠지기 쉬우니 주의하도록 하자.

'프로그래밍 > etc' 카테고리의 다른 글

Windows/Unix 간 timestamp 변환  (0) 2018.02.26

+ Recent posts