이 페이지 내용 요약

1. 문자열 관련 함수( 문자열 자르기, 문자열 <-> 숫자 변환)

2. 회문, N-gram 구현 

 

Unit 45. 문자열 자르기

45.1 문자를 기준으로 문자열 자르기

strtok 함수로 특정 문자를 기준으로 문자열을 자를 수 있다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS    // strtok 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "The Littel Prince";
    char *ptr = strtok(s1, " ");
 
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strtok(NULL" ");
    }
 
    return 0;
}
cs

strtok함수는 지정된 문자를 기준으로 문자열을 자른 다. 기준 문자는 큰 따옴표로 묶어야 한다. 위의 경우 공백 문자를 넣어 공백을 기준으로 잘랐다. 잘린 문자열을 한 번에 얻을 수 없어서 while 반복문으로 문자열을 계속 자르다가 문자열이 나오지 않으면 반복문을 끝내는 방식을 사용한다.

while 반복문 안의 strtok 함수는 자를 문자열 부분에 NULL을 넣어 주었다. NULL은 직전 strtok 함수에서 처리했던 문자열에서 잘린 문자열 만큼 다음 문자료 이동한 뒤 다음 문자열을 자른다. ptr = strtok(ptr," "); 처럼 잘린 문자열의 포인터를 다시 넣으면 다음 문자로 이동하지 못하고 처음에 나오는 문자열만 자르게 된다. strtok 함수를 사용할 때는 처음에만 자를 문자열을 넣어주고 그 다음부터는 NULL을 넣어줘야 한다.

처음 호출되는 strtok는 공백문자를 찾아서 NULL로 채운 뒤 문자열의 첫 부분인 The를 자른다.

반복문 안의 strtok에 NULL을 넣어주면 앞에서 잘린 문자열 만큼 다음 문자로 이동한 뒤 NULL로 채운뒤 Little를 자른다. 이런 식으로 반복하여 문자열 맨 끝에 있는 NULL문자를 만나면 반복문을 종료한다.

strtok 함수는 문자열을 새로 생성해서 반환하는 것이 아니라 자르는 부분으로 널문자로 채운 뒤 잘린 문자열의 포인터를 반환하는 것이다. 원본 문자열의 내용을 바꾸므로 사용에 주의해야 한다.

45.2 문자열 포인터 자르기

문자열 포인터에 문자열 리터럴이 들어 있어서 읽기 전용인 상태이면 strtok 함수는 사용할 수 없다.

문자열 포인터를 자를때는 동적 메모리를 할당하고, 문자열을 복사하여 이 문제를 해결할 수 있다.

1
2
3
4
5
6
7
8
9
char *s1 = malloc(sizeof(char* 30);    
strcpy(s1, "The Little Prince");  
char *ptr = strtok(s1, " ");   
while (ptr != NULL)
{
   printf("%s\n", ptr);
   ptr = strtok(NULL" ");
}
free(s1);    
cs

45.3 날짜와 시간값 자르기

strtok함수는 다양한 특수문자와 알파벳 영문자를 기준으로 문자열을 자를 수 있다. 또한 기준 문자는 한 번에 여러개를 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS    // strtok 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "2015-06-10T15:32:19";
    char *ptr = strtok(s1, "-T:");
 
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strtok(NULL"-T:");
    }
 
    return 0;
}
cs

-, T, : 기준으로 문자열을 자르므로 기준 문자를 여러 개 넣을 수 있다.

45.4 자른 문자열 보관하기

문자열을 자르는 while 반복문 안에서 모든 처리를 할 수 없는 상황이 있을 수 있기 때문에 자른 문자열을 보관해야 한다. 다음 코드와 같이 문자열 포인터 배열에 자른 문자열을 보관할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define _CRT_SECURE_NO_WARNINGS    // strtok 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
int main()
{
    char s1[30= "The Little Prince";
    char *sArr[10= {NULL, };
    int i = 0;
    char *ptr = strtok(s1, " ");
    while (ptr != NULL)
    {
        sArr[i] = ptr;
        i++;
        ptr = strtok(NULL" ");
    }
    for(int i = 0; i < 10; i++)
    {
        if(sArr[i] != NULL)
            printf("%s\n", sArr[i]);
    }
    return 0;   
}
cs

while 반복문 안에서는 자른 문자열의 메모리 주소를 배열에 저장하고, 배열의 인덱스를 증가시킨다.

ptr에 저장된 메모리 주소가 바뀌기 전에 다른 곳에 보관하면 자른 문자열을 나중에도 계속 사용할 수 있다. 이후 for 반복문에서 배열을 출력하였다.

*45.5 퀴즈 *

정답은 d 이다.

정답은 a, c이다. 기준 문자열은 여러 개 지정할 수 있으며 잘린 문자열의 포인터를 반환한다.

정답은 b 이다.

45.6 연습문제 : 문자열 자르기

정답은 다음 코드와 같다.

1. char *ptr = strtok(s1. " ");

2. while(ptr != NULL)

3. ptr = strtok(NULL, " ");

45.7 심사문제 : 문자열 자르기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s[61];
    scanf("%s", s);
    
    char *ptr = strtok(s, ".");
    
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strtok(NULL".");
    }
    
    return 0;
}
cs

45.8 심사문제 : 특정 단어 개수 세기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
 
int main()
{
    int cnt = 0;
    char s[1001];
    scanf("%[^\n]s", s);
    
    char *ptr = strtok(s, " .,");
    
    while(ptr != NULL)
    {
        if(strcmp(ptr, "the"== 0)
        {
            cnt++;
        }
        ptr = strtok(NULL" .,");
    }
    
    printf("%d\n", cnt);
    return 0;
}
cs

공백, 점, 쉼표를 기준으로 문자열을 잘랐고 strcmp 함수를 이용해 자른 문자열이 the 이면 cnt 변수에 1씩 누적하고, 마지막에 cnt 변수를 출력하였다.

Unit 46. 문자열과 숫자를 서로 변환하기

46.1 문자열을 정수로 변환하기

atoi 함수를 사용하면 10진법으로 표기된 문자열을 정수로 바꿀 수 있다. stdlib.h 헤더파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "283";
    int num1;
    
    num1 = atoi(s1);
    printf("%d\n", num1);
    return 0;
}
cs

atoi 함수에 문자열을 넣으면 정수가 반환된다. 문자열 정수로 되어 있어야 하며 알파벳, 특수문자 등이 포함되면 해당 문자부터는 변환하지 않는다. 또한 처음부터 숫자가 아니면 0으로 반환된다. 다음은 정수에 영문자, 특수문자가 섞여 있을 때 반환 예 이다.

46.2 특정 진법으로 표기된 문자열을 정수로 변환하기

strtol 함수를 사용하여 16진법으로 표기된 문자열을 정수로 바꿀 수 있다. strtol(문자열, 끝포인터, 진법)의 형태로 사용한다. stdlib.h 헤더파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "0xaf10";
    int num1;
 
    num1 = strtol(s1, NULL16);
    printf("%x %d\n", num1, num1);
    return 0;
}
cs

변환할 문자열을 저장한 배열을 넣어주고 16을 지정하면 16진법으로 표기된 문자열을 정수로 변환할 수 있다. 10을 지정하면 10진법으로 표기된 문자열을 정수로 변환할 수 있다.

두 번째 인수는 여러개의 정수로 된 문자열을 변환할 때 사용한다. 여러개의 정수로된 문자열을 각각 변환하는 것은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "0xaf10 42 0x27C 9952";
    int num1;
    int num2;
    int num3;
    int num4;
    char *end;
 
    num1 = strtol(s1, &end16);
    num2 = strtol(end&end10);
    num3 = strtol(end&end16);
    num4 = strtol(endNULL10);
 
    printf("%x\n%d\n%X\n%d\n", num1,num2,num3,num4);
    return 0;
}
cs

s1에는 16진법으로 표기된 숫자 2개와 10진법으로 표기된 숫자 2개사 있다. 두 번째 인수로 끝 포인터를 선언하여 메모리 주소를 넣으면 strtol 함수가 실행된 뒤에는 끝 포인터가 " 42 0x27C 9952" 처럼 이전 숫자의 끝 부분부터 시작하게 된다.

두번째 부터는 문자열에 end를 넣어 이전 숫자의 끝 부분부터 변환하면 된다. 이 과정을 그림으로 확인하면 다음과 같다.

46.3 문자열을 실수로 변환하기

atof 함수를 사용하여 문자열을 실수로 바꿀 수 있다. stdlib.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "35.212345";
    float num1;
 
    num1 = atof(s1);
 
    printf("%f\n", num1);
    return 0;
}
cs

atof 함수에 문자열을 넣으면 실수가 반환된다. 문자열은 실수로 되어있어야 하며 알파벳 영문자, 특수문자가 포함되면 해당 문자부터는 변환을 하지 않는다. 또한 처음부터 숫자가 아니면 0으로 변환된다.

다음과 같이 알파벳 e를 사용한 지수 표기법으로 된 문자열도 실수로 바꿀 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "3.e5";
    float num1;
 
    num1 = atof(s1);
 
    printf("%e %f\n", num1, num1);
    return 0;
}
cs

strtof 함수를 사용하여 여러개의 실수로 된 문자열을 실수로 바꿀수 있다. stdlib.h 헤더파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>   
 
int main()
{
    char *s1 = "35.283672 3.e5 9.281772 7.e-5";    
    float num1;
    float num2;
    float num3;
    float num4;
    char *end;   
 
    num1 = strtof(s1, &end);     
    num2 = strtof(end&end);    
    num3 = strtof(end&end);   
    num4 = strtof(endNULL);    
 
    printf("%f\n", num1);   
    printf("%e\n", num2);    
    printf("%f\n", num3);  
    printf("%e\n", num4);    
    return 0;
}
cs

처음에는 s1을 넣어 문자열을 실수로 변환하고, 끝 포인터는 &end처럼 메모리 주소를 넣어 strtof함수가 실행된 후 끝 포인터가 이전 숫자의 끝 부분부터 시작하게 했다. 두 번째 부터는 end를 넣어 이전 숫자의 끝 부분버터 변환한다. double 형 실수로 변환하는 strtod 함수도 있다.

46.4 정수를 문자열로 변환하기

sprintf 함수를 사용하여 정수를 문자열로 변환할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char s1[10];
    int num1 = 283;
 
    sprintf(s1, "%d", num1);
    printf("%s", s1);
    return 0;
}
cs

변환한 문자열을 저장할 배열을 선언하고, sprintf 함수에 서식지정자로 %d를 설정한 뒤 정수를 문자열로 저장하였다. atoi를 반대로 변환하는 itoa 같은 함수도 있지만 C 표준 함수는 아니다.

다음과 같이 16진법으로 표기된 문자열로 변환하려면 서식지정자 %x를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char s1[10];
    int num1 = 283;
 
    sprintf(s1, "0x%x", num1);
    printf("%s\n", s1);
    return 0;
}
cs

sprintf 함수에서 서식지정자로 %x를 사용하면 16진법으로 표기된 문자열로 변환할 수 있다. 16진수라는 것을 명확하게 나타내기 위해 앞에 0x를 붙여준 것이다. 서식지정자를 %X로 지정하면 16진수 알파벳 부분이 대문자로 저장된다.

46.5 실수를 문자열로 변환하기

실수를 문자열로 변환할 때도 sprintf 함수를 사용한다. 다음과 같이 서식지정자로 %f를 지정하면 된다. 또한 %e를 지정하여 지수 표기법으로 된 문자열로 변환할 수도 있다.

sprintf(s1, "%f", num1);
sprintf(s1, "%e", num1);

변환된 문자열이 길어질 수 있기 때문에 배열의 크기 또는 동적메모리로 할당한 크기를 잘 확인해야 한다.

46.6 퀴즈

정답은 b이다.

정답은 b이다.

정답은 c 이다.

정답은 e 이다.

정답은 c, g 이다.

정답은 b이다.

46.7 연습문제: 문자열을 10진 정수로 변환하기

정답은 다음 코드와 같다.

1. #include <stdlib.h>

2. num1 = atoi(s1);

46.8 연습문제 : 문자열을 16진 정수로 변환하기

정답은 다음 코드와 같다.

1. num1 = strtol(s1, NULL, 16)

2. "0x%X\n"

대문자로 출력하므로 서식지정자 %X를 사용한다.

46.9 연습문제 : 문자열을 실수로 변환하기

정답은 다음과 같다.

1. "97.527824"

2. num1

46.10 연습문제: 여러개의 실수로 된 문자열을 실수로 변환하기

정답은 다음 코드와 같다.

1. num1 = strtof(s1, &end);

2. num2 = strtof(end, NULL);

46.11 연습문제 : 숫자를 문자열로 변환하기

정답은 다음 코드와 같다.

1. char s1[30];

2. sprintf(s1, "%f 0x%x", num1, num2);

46.12 심사문제 : 문자열을 정수와 실수로 변환하기

정답은 다음 코드와 같다.

num1 = strtol(s1, &end, 16);
num2 = strtol(end, &end, 10);
num3 = strtof(end, NULL);

46.3 심사문제 : 정수와 실수를 문자열로 변환하기

정답은 다음 코드와 같다.

sprintf(s1, "%d", num1);
sprintf(s2, "%f", num2);

Unit 47. 회문 판별과 N-gram 만들기

회문은 유전자 염기 서열 분석에서 많이 쓰이고, N-gram은 빅데이터 분석, 검색 엔진에서 많이 쓰인다.

47.1 회문 판별

회문은 level, SOS 등과 같이 순서를 거꾸로 읽어도 제대로 읽은 것과 같은 단어와 문장을 말한다.

회문인지 판별하려면 첫번째 글자와 마지막 글자가 같고, 안쪽으로 한 글자씩 좁혔을 때 글자가 서로 같으면 회문이다.

다음은 문자열을 배열에 넣은뒤 반복문으로 각 글자를 검사한 것이다.

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
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
 
int main()
{
    char word[30];
    int length;
    bool isPalindrome = true;
 
    printf("단어를 입력하세요 : ");
    scanf("%s", word);
 
    length = strlen(word);
 
    for(int i = 0; i < length / 2; i++)
    {
        if(word[i] != word[length - 1 -i])
        {
            isPalindrome = false;
            break;
        }
    }
 
    printf("%d\n", isPalindrome);
    return 0;
}
cs

회문일 경우 1이 출력되고, 회문이 아닐 경우 0이 출력된다.

회문 판별에서 가장 중요한 것은 문자열의 길이이다.

문자열 길이의 절반만큼 반복하며 왼쪽 문자와 오른쪽 문자들을 검색한다. 반복문 안에서 왼쪽문자와 오른쪽 문자를 비교하며 문자가 다르면 isPalindrome 에 false를 넣고 반복문을 끝낸다. 문자열의 마지막 문자는 word[length-1] 이므로 인덱스를 i 만큼 빼주면 오른쪽에서 왼쪽으로 진행할 수 있다.

47.2 N-gram 만들기

N-gram은 문자열에서 N개의 연속된 요소를 추출하는 방법이다. Hello의 경우 2-gram으로 추출하면 다음과 같다.

문자열의 처음부터 끝까지 한 글자씩 이동하며 2 글자씩 추출한다.

#include <stdio.h>
#include <string.h>

int main()
{
    char text[30] = "Hello";
    int length;

    length = strlen(text);

    for(int i = 0; i < length - 1; i++)
    {
        printf("%c%c\n", text[i], text[i+1]);
    }

    return 0;
}

2-gram이므로 문자열의 끝에서 한 글자 앞까지만 반복하며 현재 문자와 그 다음문자 두 글자씩 출력한다.

만약 3-gram이라면 조건식은 i < length -2가 될 것이고, 문자열 끝에서 두 글자 앞까지 반복하면 된다.

단어 단위 N-gram도 있다. 다음은 공백을 기준으로 구분하여 단어단위 2-gram을 출력하는 코드이다.

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
#include <stdio.h>
#include <string.h>
 
int main()
{
    char text[100= "this is c language";
    char *tokens[30= {NULL, };
 
    int cnt = 0;
    char *ptr = strtok(text, " ");
 
    while (ptr != NULL)
    {
        tokens[cnt] = ptr;
        cnt++;
        ptr = strtok(NULL" ");
    }
 
    for(int i = 0; i < cnt -1; i++)
    {
        printf("%s %s\n", tokens[i], tokens[i+1]);
    }
 
    return 0;
}
cs

strtok 함수로 문자열을 자른 뒤 각 단어들을 배열에 넣고, 배열의 마지막에서 요소 한 개 앞까지만 반복하면서 현재 문자열과 그 다음 문자열을 출력하면 된다.

47.3 연습문제 : 정수 회문 판별하기

정답은 다음 코드와 같다.

1. sprintf(text, "%lld", num1)

2. begin++;

3. end--;

입력값이 정수 이므로 sprintf 함수로 문자열로 변환하여 회문을 판별한다.

47.4 연습문제 : 4-gram 만들기

정답은 다음 코드와 같다.

1. length < n

2. i < length - (n-1)

3. j < n

문자열을 반복할 때는 배열의 범위를 벗어나지 않도록 i < length(n-1)로 조건식을 사용했다.

47.5 심사문제 : 공백이 포함된 회문 판별

정답은 다음 코드와 같다.

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
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
 
int main()
{
    char str[31];
    int length;
    bool isP = true;
    
    scanf("%[^\n]s", str);
    length = strlen(str);
    
    for(int i = 0, j = length-1; i < j; i++, j--)
    {
        while(str[i] == ' ')
            i++;
        while(str[j] == ' ')
            j--;
        if(str[i] != str[j])
        {
            isP = false;
            break;
        }
    }
    printf("%d\n", isP);
    return 0;
}
cs

반복문에서 문자열의 처음부터, 끝에서부터 반복시키며 공백이 있을 경우 한 칸씩 증가, 감소 시켜서 공백을 지우고, 문자열 앞에서 부터 온 문자와 뒤에서 부터 온 문자가 다르면 회문을 거짓으로 하고, 반복문을 끝낸다.

47.6 심사문제 : N-gram 만들기

정답은 다음 코드와 같다.

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
#include <stdio.h>
#include <string.h>
 
int main()
{
    char text[11];
    int n;
    scanf("%d %s"&n, text);
    int length = strlen(text);
    
    if(length < n)
    {
        printf("wrong\n");
    }
    else
    {
        for(int i = 0; i < length - (n-1); i++ )
        {
            for(int j = 0; j < n; j++)
            {
                printf("%c", text[i+j]);
            }
            printf("\n");
        }
    } 
    
    return 0;
}
cs

2중 for문의 안쪽 반복문은 입력한 숫자만큼 반복하므로 입력한 숫자만큼의 문자를 출력할 수 있고, 바깥쪽 반복문으로 1씩 증가시키면서 문자열에서 문자를 한 칸씩 앞으로 가도록 했다.

 

 

이번 페이지 요약

코딩도장 C언어 unit 41~44

1. 문자열 관련 함수(길이, 복사, 문자열 만들기, 검색)

 

Unit 41. 문자열의 길이를 구하고 비교하기

41.1 문자열의 길이 구하기

문자열의 길이는 strlen 함수로 구할 수 있으며 string.h 헤더 파일에 선언되어 있다. 문자열 포인터와 문자 배열의 길이 모두 구할 수 있으며 사용법은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>
 
int main()
{
    char *s1 = "Hello";
    char s2[10= "Hello";
 
    printf("%d\n", strlen(s1));
    printf("%d\n", strlen(s2));
    return 0;
}
cs

strlen에 문자열 포인터나 문자 배열을 넣으면 문자열의 길이가 반환된다 Hello는 5글자 이기 때문에 5가 출력되었으며 NULL 부분은 포함하지 않는다. 배열의 경우 배열의 크기는 관계없이 문자열의 길이만 구한다. 위 코드의 경우도 배열의 크기는 10이지만 출력값은 문자열의 길이인 5가 나왔다.

41.2 문자열 비교하기

strcmp 함수를 사용하면 두 문자열이 같은 지 비교할 수 있다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
 
int main()
{
    char *s1 = "Hello";
    char s2[10= "Hello";
    
    int ret = strcmp(s1, s2);
 
    printf("%d\n", ret);
    return 0;
}
cs

strcmp 함수에 비교할 문자열을 넣으면 결과를 정수로 반환하며 반환 되는 값의 종류는 다음과 같다.

  • -1: ASCII 코드 기준으로 문자열2(s2)가 클 때
  • 0: ASCII 코드 기준으로 두 문자열이 같을 때
  • 1: ASCII 코드 기준으로 문자열1(s1)이 클 때

배열 문자열, 문자열 포인터 등 문자열의 저장 방식은 문자열 비교에 영향을 주지 않는다.

strcmp 함수는 문자열에서 첫 번째 문자부터 차례대로 비교하며 비교 기준은 각 문자의 ASCII 코드이다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <string.h>
 
int main()
{
    printf("%d\n", strcmp("aaa""aaa"));
    printf("%d\n", strcmp("aab""aaa"));
    printf("%d\n", strcmp("aab""aac"));
    return 0;
}
cs

aaa는 아스키 코드로 97 97 97 이고, aab 는 97 97 98 이기 때문에 aab 가 더 크다. 또한 aac는 97 97 99 이므로 aac가 aab 보다 크다. 앞의 것이 크면 1, 뒤의 것이 크면 -1을 반환한다. 이것은 윈도우 기준이고, 리눅스나 OS X에서는 코드값의 차이를 반환한다. 따라서 입력받는 두 문자열의 크기를 비교할 땐 윈도우에서는 switch문을 사용하면 되고, 리눅스나 OS X 에서는 if 문을 사용해야 한다.

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
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[20];
    char s2[20];
 
    printf("문자열 두 개를 입력하세요 : ");
    scanf("%s %s", s1, s2);
 
    int ret = strcmp(s1, s2);
    printf("반환값 : %d\n", ret);
 
    if(ret == 0)
    {
        printf("같음\n");
    }
    else if(ret > 0)
    {
        printf("%s가 %s보다 큼\n", s2, s1);
    }
    else if(ret < 0)
    {
        printf("%s가 %s보다 큼\n", s1, s2);
    }
 
    return 0;
}
cs

리눅스나 OS X에서 strcmp는 문자열1에서 문자열 2의 아스키 코드값을 뺏을 때 양수가 나오면 1이 크고, 음수가 나오면 2가 크다. 두 문자열이 같으면 0이 나온다.

41.3 퀴즈

정답은 c 이다.

정답은 c,d,f 이다.

41.4 연습문제 : 문자열 길이 구하기

정답은 strlen(s1) 이다.

41.5 연습문제 : 문자열 비교하기

정답은 char *s1 = "Pachelbel Canon"; 이다.

41.6 심사문제 : 문자열 길이 구하기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s[30];
    scanf("%s", s);
    printf("%d", strlen(s));
    return 0;
}
cs

41.7 심사문제 : 문자열 비교하기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30];
    char s2[30];
    
    scanf("%s %s", s1, s2);
    
    printf("%d", strcmp(s1, s2));
    return 0;
}
cs

Unit 42. 문자열을 복사하고 붙이기

42.1 문자열 복사하기

strcpy 함수를 이용하여 문자열을 다른 배열이나 포인터(메모리)로 복사할 수 있다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS    // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[10= "Hello";
    char s2[10];
 
    strcpy(s2, s1);
 
    printf("%s\n", s2);
    return 0;
}
cs

배열 s2에는 아무것도 들어있지 않았지만 strcpy 함수로 s1의 문자열이 복사되어 s2에도 저장되었다.

복사된 결과가 저장된 배열의 크기는 반드시 NULL까지 들어갈 수 있는 크기여야 한다. 따라서 Hello 라는 문자열이 복사되려면 NULL문자까지 크기는 최소 6 이상이 되어야 한다.

문자열 포인터는 복사할 공간도 없으며 읽기만 가능하기 때문에 문자열을 복사할 수 없다. 문자열 포인터에 문자열을 복사하려면 다음과 같이 메모리를 할당한 뒤 복사해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define _CRT_SECURE_NO_WARNINGS    // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "Hello";
    char *s2 = malloc(sizeof(char* 10);
 
    strcpy(s2, s1);
 
    printf("%s\n", s2);
    free(s2);
    return 0;
}
cs

s2에 char 10개 크기만큼 동적 메모리를 할당하여 문자열을 복사하고, free로 할당한 메모리를 해제하였다.

42.2 문자열 붙이기

문자열은 strcat 함수를 이용하여 서로 붙일 수 있다. string.h 헤더파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS    // strcat 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[10= "world";
    char s2[20= "Hello";
 
    strcat(s2, s1);
 
    printf("%s\n", s2);
    return 0;
}
cs

strcat함수에 최종 결과가 나올 문자열과 붙일 문자열을 넣는다. 위의 경우 s2 뒤에 s1 이 붙어서 Helloworld가 나온다.

문자열을 붙이더라도 배열이 모자라지 않도록 최종 결과가 나올 문자열의 배열 크기를 넉넉하게 만들어야 한다.

문자열 포인터에 사용하려면 붙일 문자열 포인터에 동적 메모리를 할당해 줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS  
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "world";
    char *s2 = malloc(sizeof(char* 20);
 
    strcpy(s2, "Hello");
    strcat(s2, s1);
 
    printf("%s\n", s2);
    free(s2);
    return 0;
}
cs

최종 결과가 나올 문자열 포인터에 char 20개 크기만큼 동적 메모리를 할당하고, 메모리가 할당된 문자열 포인터는 문자열을 직접 할당할 수 없으므로 strcpy 함수를 이용하여 "Hello"를 복사하고, s2 뒤에 s1을 붙인다. 문자열 사용이 끝났으면 free로 동적 메모리를 해제한다.

42.3 배열 형태의 문자열을 문자열 포인터에 복사하기

다음과 같이 배열 형태의 문자열을 문자열 포인터로 복사할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define _CRT_SECURE_NO_WARNINGS  
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main()
{
    char s1[10= "Hello";
    char *s2 = malloc(sizeof(char* 10);
 
    strcpy(s2, s1);
 
    printf("%s\n", s2);
    free(s2);
    return 0;
}
cs

malloc 함수로 s2에 메모리를 복사할 문자열이 충분히 들어갈 수 있을 정도로 할당하고, strcpy로 복사하면 된다.

42.4 배열 형태의 문자열을 문자열 포인터에 붙이기

다음과 같이 배열 문자열을 문자열 포인터에 붙일 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS   
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main()
{
    char s1[10= "world";
    char *s2 = malloc(sizeof(char* 20);
 
    strcpy(s2, "Hello");
    strcat(s2, s1);
 
    printf("%s\n", s2);
    free(s2);
    return 0;
}
cs

문자열 포인터 s2 뒤에 문자열을 붙여야 하므로 메모리가 부족하지 않도록 넉넉히 할당하고, strcpy함수로 s2에 "Hello"를 복사한다. 이후 s2 뒤에 strcat 함수로 문자열 배열 s1을 붙이면 된다. 문자열 사용이 끝나면 free로 메모리를 해제해줘야 한다.

42.5 퀴즈

정답은 NULL문자까지 6이다.

정답은 NULL문자까지 11바이트 이다.

정답은 c 이다.

42.6 연습문제 : 문자열 포인터를 배열에 복사하기

문자열을 복사하는 것이므로 strcpy 함수를 사용하여 정답은 strcpy(s2, s1); 이다.

42.7 연습문제 : 문자열 포인터를 동적 메모리에 복사하기

정답은 다음 코드와 같다.

1. malloc(sizeof(char) * 20);

2. strcpy(s2, s1);

42.8 연습문제 : 문자 배열을 붙이기

정답은 strcat(s2, s1); 이다.

42.9 연습문제 : 문자열 리터럴과 동적 메모리 붙이기

정답은 다음 코드와 같다.

1. strcpy(s2, "Alice in");

2. strcat(s2, s1);

42.10 심사문제 : 문자 배열 복사하기

정답은 다음 코드와 같다.

scanf("%s", s1);
strcpy(s2, s1);

s1에는 값을 입력받고, s2에는 s1의 값을 복사해서 가져왔다.

42.11 심사문제 : 두 문자열 붙이기

scanf("%s", s1);
strcat(s1, "th");

문자열을 입력받아 strcat 함수로 문자열 끝에 th를 붙였다.

Unit 43. 문자열 만들기

43.1 서식을 지정하여 배열 형태로 문자열 만들기

sprintf 함수를 사용하면 서식을 지정하여 문자열을 만들 수 있다. sprintf(배열, 서식, 값);의 형태로 사용하며 서식에 값이 들어간 것을 배열에 저장할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#define _CRT_SECURE_NO_WARNINGS    // sprintf 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
 
int main()
{
    char s1[20];
 
    sprintf(s1, "Hello %s""world!");
 
    printf("%s\n", s1);
    return 0;
}
cs

sprintf 함수에 문자열을 저장할 배열과 문자열을 만들 서식, 문자열을 만들 값(문자열)을 순서대로 넣어 %s부분이 "word!"로 바뀌게 된다. 위 코드에서 s1과 같이 문자열을 저장할 빈 배열을 버퍼(buffer)라고 부른다. 서식지정자로 C언어의 다양한 자료형들을 문자열로 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
 
int main()
{
    char s1[30];
 
    sprintf(s1, "%c %d %f %e"'a'103.2f, 1.123456e-21f);
    printf("%s\n", s1);
    return 0;
}
cs

sprintf()함수에 %c, %d, %f, %e를 지정하여 각 서식지정자에 맞는 문자, 정수, 소수점 표기 실수, 지수 표기법 실수를 넣었다. 위 코드에서는 함수에 값을 바로 넣었지만, 변수를 넣어도 된다.

43.2 서식을 지정하여 문자열 포인터에 문자열 만들기

문자열 포인터를 사용하려면 malloc 함수로 메모리를 할당한 뒤 sprintf함수로 문자열을 만들면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = malloc(sizeof(char* 20);
    sprintf(s1, "Hello, %s""world!");
    
    printf("%s\n", s1);
    free(s1);
    return 0;
}
cs

char 20개 크기의 동적 메모리를 할당하고 sprintf함수에 서식을 지정하여 문자열을 만들면 된다. 배열과 마찬가지로 문자열을 생성할 메모리 공간은 버퍼이다. 문자열 사용이 끝나면 free 함수 동적 할당한 메모리를 해제 해야 한다.

다음과 같이 다양한 종류의 서식지정자를 사용하여 다양한 자료형도 문자열로 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = malloc(sizeof(char* 30);
    sprintf(s1, "%c %d %f %e"'a'103.1f, 1.23e-23f);
    
    printf("%s\n", s1);
    free(s1);
    return 0;
}
cs

char 30개 크기만큼 동적으로 메모리를 할당하고, sprintf함수에 서식을 지정하여 문자, 정수, 소수점 표기 실수, 지수 표기 실수를 문자열로 만들었다. 문자열 사용이 끝나면 free함수로 할당한 메모리를 해제해야 한다.

43.3 퀴즈

정답은 d이다.

정답은 "%c %d" 이다.

정답은 버퍼 이다.

43.4 연습문제 : 숫자와 문자열을 조합하여 문자열 만들기

숫자와 문자열이 순서대로 들어가므로 정답은 9, "Symphony" 이다.

43.5 연습문제 : 서식에 맞게 문자열 만들기

정수 3개 ,문자 1개 정수 1개가 순서대로 들어가므로 정답은 "%d %d %d %c %d" 이다.

43.6 심사문제 : 서수 줄임말 문자열 만들기

정답은 다음코드와 같다.

scanf("%d %s", &number, name);

if(number == 1)
    sprintf(result, "%dst %s", number, name);
else if(number == 2)
    sprintf(result, "%dnd %s", number, name);
else if(number == 3)
    sprintf(result, "%drd %s", number, name);
else
    sprintf(result, "%dth %s", number, name);

숫자에 따라 숫자 뒤에 붙는것이 다르기 때문에 if 조건문을 사용하여 조건에 맞게 출력하도록 하였다.

Unit 44. 문자열 검색하기

44.1 문자열 안에서 문자로 검색하기

strchr 함수를 사용하여 문자열에서 특정 문자로 검색할 수 있다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "A Garden Diary";
    char *ptr = strchr(s1, 'a');
 
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strchr(ptr + 1'a');
    }
    return 0;
}
cs

strchr 함수에 문자열과 검색할 문자를 넣어주면 해당 문자로 시작하는 문자열의 위치(포인터)를 반환한다.

'a' 로 시작하는 부분을 더 찾기 위해 while 반복문을 사용하여 검색된 문자여릐 포인터에 1을 더해 다음부터 또 검색해서 찾는다. NULL이 나오면 검색할 문자열이 없는 것이므로 반복을 끝낸다.

'a' 가 들어간 문자를 처음 찾으면 arden Diary를 찾을 수 있다. 포인터에 1을 더하면 rden Diary 가 되므로 다음번 a를 찾을 수 있게 된다.

strchr 함수는 대소문자를 구분하므로 A는 찾지 않고 넘어간다.

44.2 문자열의 오른쪽 끝부터 문자로 검색하기

strrchr 함수는 문자열의 끝에서 부터 문자를 검색한다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "A Garden Diary";
    char *ptr = strrchr(s1, 'a');
 
    printf("%s\n", ptr);
    return 0;
}
cs

strrchr 함수에 문자열과 검색할 문자를 넣으면 문자열 끝에서 부터 검색하여 'a'로 시작하는 문자열의 포인터를 반환한다. strrchr함수도 대소문자를 구분한다.

44.3 문자열 안에서 문자열로 검색하기

strstr 함수는 문자열 안에서 문자열을 검색할 수 있는 함수이며 string.h 헤더 파일에 선언되어 있다.

워드나 메모장에서 검색할 때 단어로 검색하는 경우가 많은것처럼 프로그램을 만들때도 문자열 찾는 함수를 더 많이 사용한다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "A Garden Diary";
    char *ptr = strstr(s1, "den");
 
    printf("%s\n", ptr);
    return 0;
}
cs

strstr 함수에 검색할 문자열을 넣으면 해당 문자열로 시작하는 문자열의 위치(포인터)를 반환한다. 검색한 문자만 나오지 않고, 검색한 문자를 포함하여 뒤에 오는 모든 문자열이 나온다. strstr 함수도 대소문자를 구분하며 다음과 같이 반복문을 사용하여 문자열을 계속 검색할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[100= "A Garden Diary A Garden Diary A Garden Diary";
    char *ptr = strstr(s1, "den");
 
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strstr(ptr+1"den");
    }
    return 0;
}
cs

44.4 퀴즈

정답은 c, e 이다.

정답은 b 이다.

44.5 연습문제 : 문자열 안에서 문자로 검색하기

정답은 다음 코드와 같다.

1. char *ptr = strchr(s1, 'n');

2. while(ptr != NULL)

3. ptr = strchr(ptr + 1, 'n');

44.6 연습문제 : 문자열의 오른쪽 끝 부터 문자로 검색하기

정답은 char *ptr = strrchr(s1, 'i'); 이다. 마지막의 ince만 출력하려면 오른쪽 부터 검색하는 strrchr 함수를 사용해야 한다.

44.7 심사문제 : 공백 개수 세기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
 
int main()
{
    char str[1001];
    scanf("%[^\n]s", str);
    char *ptr = strchr(str, ' ');
    int cnt = 0;
    
    while(ptr != NULL)
    {
        cnt++;
        ptr = strchr(ptr + 1' ');
    }
    printf("%d\n", cnt);
    return 0;
}
cs

정수형 변수를 선언한 후 공백을 찾을 때 마다 1씩 누적하고 반복문이 끝난 뒤 누적한 값을 출력하였다.

 

이번 페이지 요약 : 

코딩도장 C언어 Unit 39, 40

1. 문자열 사용하기

2. 입력값 문자열에 저장 

 

Unit 39. 문자열 사용하기

c언어 문자를 저장하는 자료형은 있지만 문자열을 저장하는 자료형은 없다. 그래서 char 자료형에 문자열을 저장하면 컴파일은 되지만 실행은 되지 않는다. 컴파일을 한 뒤 디버깅 모드로 보면 다음과 같은 에러가 발생한다.

39.1 문자와 문자열 포인터 알아보기

문자열은 다음과 같이 char 포인터 형식으로 사용한다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char c1 = 'a';
    char *s1 = "Hello";
 
    printf("%c\n", c1);
    printf("%s\n", s1);
    return 0;
}
cs

문자는 글자가 하나만 있는 상태이고, 문자열은 글자 여러 개가 계속 이어진 상태를 의미한다. 문자는 1바이트 크기라 char에 저장할 수있지만, 문자열은 1바이트가 넘어서므로 char에 저장할 수 없다. 따라서 문자열은 변수에 직접 저장하지 않고 포인터를 이용하여 저장한다. 포인터는 메모리 주소를 저장하기 때문에 메모리 주소를 보고 문자열이 저장된 위치로 이동한다. 문자열 리터럴이 있는 메모리 주소는 읽기 전용이기 때문에 다른 문자열을 덮어 쓸 수 없다. 문자열 리터럴이 저장되는 위치는 컴파일러가 결정한다.

문자열은 ""(큰따옴표)로 묶어야 하며 문자열을 출력할 때는 서식지정자 %s를 사용해야 한다.

C에서 문자열은 마지막 끝에 항상 널문자가 붙는다. NULL은 0으로 표현할 수 있으며 문자열의 끝을 나타낸다.

문자열을 출력할때는 문자들을 순서대로 계속 출력하다가 NULL을 만나면 출력을 끝낸다.

39.2 문자열 포인터에서 인덱스로 문자에 접근하기

문자열도 다음과 같이 대괄호를 사용하여 인덱스로 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char *s1 = "Hello";
 
    printf("%c\n", s1[1]);
    printf("%c\n", s1[4]);
    printf("%c\n", s1[5]);
    return 0;
}
cs

문자 하나를 출력할 때는 %c 서식지정자를 사용하며 맨 뒤 문자열인 5번째 인덱스를 가져오면 NULL 이기 때문에 출력해도 화면에 표시되지 않는다.

문자열 포인터에서 인덱스로 문자를 가져오는 것은 가능하지만 인덱스로 문자를 할당하는것은 불가능하다. 문자를 할당하려고 에러가 발생한다. 문자열 리터럴이 있는 메모리 주소는 읽기 전용이기 때문이다.

38.3 배열 형태로 문자열 선언하기

문자열은 다음과 같이 문자 배열에 저장할 수 도 있다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main()
{
    char s1[10= "Hello";
    
    printf("%s\n", s1);
    return 0;
}
cs

위 코드는 크기가 10인 배열을 선언한 뒤 문자열을 할당한 것이다.

문자열에 배열을 저장하는 방식은 배열 요소 하나하나에 문자가 저장된다. 배열이기 때문에 인덱스는 0부터 시작하고, 문자열의 맨 뒤에 NULL이 들어간다. 남는 공간에도 모두 NULL이 들어간다.

배열로 문자열을 사용할 때는 무조건 배열을 선언한 즉시 문자열로 초기화해야 한다. 이미 선언한 배열에는 문자열을 할당할 수 없으며 정 할당하고 싶으면 요소에 문자 하나 하나를 넣으면 된다.

배열을 선언할 때는 배열의 크기를 할당할 문자열보다 크게 지정해야 한다. 문자열 문자 개수보다 배열의 크기가 작다면 컴파일은 되지만 문자열이 제대로 출력되지 않는다. 문자열은 마지막에 NULL이 꼭 들어가야 하므로 문자 개수에 하나 더한 크기가 문자열을 저장할 수 있는 배열의 최소 크기 이다.

다음과 같이 문자열을 바로 할당할 때는 배열의 크기를 생략할 수 있다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main()
{
    char s1[] = "Hello";
    
    printf("%s\n", s1);
    return 0;
}
cs

이와 같은 경우 배열의 크기는 문자열의 문자 개수에 맞게 자동으로 지정된다. 위 경우 문자 5개에 NULL을 더해 6이 된다.

39.4 배열 형태의 문자열에서 인덱스로 문자에 접근하기

다음과 같이 인덱스로 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char s1[] = "Hello";
    
    printf("%c\n", s1[1]);
    printf("%c\n", s1[4]);
    printf("%c\n", s1[5]);
    return 0;
}
cs

배열을 인덱스로 접근하면 char과 같기 때문에 %c로 출력할 수 있다. 배열로 저장된 문자열 맨 뒤에는 NULL이 있어 출력값에는 보이지 않는다.

배열 형태의 문자열은 다음과 같이 인덱스로 접근하여 문자를 할당할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char s1[] = "Hello";
    
    s1[0= 'A';
    
    printf("%s\n", s1);
    return 0;
}
cs

Hello 가 들어있는 문자 배열의 0번 인덱스에 A를 할당해서 Aello가 되었다. 배열 형태의 문자열은 배열에 문자열이 모두 복사되었기 때문에 인덱스로 접근하여 내용을 변경할 수 있다.

39.5 퀴즈

정답은 b,e,g,h이다.

정답은 d 이다.

정답은 b 이다.

정답은 a, d 이다.

39.6 연습문제 : 문자열 만들기

정답은 char s1[] 이다.

39.7 연습문제 : 문자열 요소 출력

정답은 s1[10] 이다.

39.8 심사문제 : 문자열 만들기

정답은 다음 코드와 같다.

char s1[] = "Beethoven\n9th\nSymphony";

출력에 줄바꿈이 있으므로 개행문자 \n을 사용했다.

Unit 40. 입력 값을 문자열에 저장하기

40.1 입력 값을 배열 형태의 문자열에 저장하기

scanf 함수에서 서식지정자 %s를 사용하면 입력값을 배열 형태의 문자열에 저장할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main()
{
    char s1[10];
 
    printf("문자열을 입력하세요 : ");
    scanf("%s", s1);
 
    printf("%s\n", s1);
    return 0;
}
cs

scanf 함수에 서식지정자로 %s를 넣어서 문자열을 입력받을 수 있다. 매개변수는 입력값을 저장할 배열을 넣는데, 배열 앞에는 일반 변수와 달리 &를 붙이지 않는다. 위 코드에서는 크기가 10인 배열을 선언했으므로 문자열 맨 뒤에 붙는 널문자 까지 포함하여 총 10개의 문자를 저장할 수 있다. 따라서 실제 저장할 수 있는 문자는 9개이다.

scanf에서 %s로 문자열을 저장할 때 입력된 문자열에 공백이 있다면 배열에는 공백 직전까지만 저장된다. 공백을 포함하여 저장하고 싶다면 서식지정자로 %[^\n]s를 사용하면 된다.

40.2 입력 값을 문자열 포인터에 저장하기

문자열 포인터를 선언한 뒤 scanf 함수로 입력 값을 문자열 포인터에 저장하고 실행하면 에러가 발생한다. 이미 할당된 문자열 포인터에 저장된 메모리 주소는 읽기만 할 수 있고, 쓰기가 막혀있기 때문이다.

입력값을 문자열 포인터에 저장하려면 문자열이 들어갈 공간을 따로 만들어야 한다. malloc 함수로 메모리를 할당한 뒤 문자열을 저장하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = malloc(sizeof(char* 10);
    
    printf("문자열을 입력하세요 : ");
    scanf("%s", s1);
 
    printf("%s\n", s1);
    free(s1);
    return 0;
}
cs

char 10개 크기 만큼 동적 메모리를 할당하여 입력값을 문자열 포인터 s1에 저장할 수 있다. 입력 받을 때 s1은 포인터이므로 &를 붙이지 않는다. 문자열 사용이 끝나면 free 함수로 메모리를 해제해야 한다. 문자열 중간에 공백이 있다면 문자열 포인터에는 공백 직전까지만 저장된다.

40.3 문자열을 여러 개 입력받기

다음과 같이 공백으로 구분된 문자열 여러개를 입력받을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    char s1[10];
    char s2[10];
 
    printf("문자열을 두 개 입력하세요 : ");
    scanf("%s %s", s1, s2);
 
    printf("%s\n", s1);
    printf("%s\n", s2);
    return 0;
}
cs

문자열 두 개를 저장하기 위한 공간을 선언하고, scanf 함수에서 서식지정자로 %s를 두 개 넣어준다. 문자열이 저장될 배열을 두 개 넣어주면 공백으로 구분된 문자열을 입력받을 수 있다. 문자열을 더 입력받고 싶으면 %s와 입력받을 배열(또는 문자열 포인터)의 개수를 늘려주면 된다.

40.4 퀴즈

정답은 c, e, g 이다.

정답은 9 이다.

40.5 연습문제 : 입력받은 문자열을 배열에 저장하기

정답은 scanf("%s", s1); 이다.

40.6 연습문제 : 입력받은 문자열을 동적 메모리에 저장하기

정답은 다음 코드와 같다.

1. char *s1 = malloc(sizeof(char) * 10);

2. scanf("%s", s1);

40.7 연습문제 : 문자열 세 개 입력받기

정답은 scanf("%s %s %s", s1, s2, s3); 이다.

40.8 심사문제 : 문자열 4 개 입력받기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    char s1[30];
    char s2[30];
    char s3[30];
    char s4[30];
    
    scanf("%s %s %s %s", s1, s2, s3, s4);
    
    printf("%s\n%s\n%s\n%s\n", s1, s2, s3, s4);
    return 0;
}
cs

해당 글의 원문은 아래 주소에서 확인할 수 있다.

bpsecblog.wordpress.com/2016/05/20/gdb_memory_3/

 

(完)우리집에 GDB 있는데… 메모리 보고갈래?(3)

우리집에 GDB 있는데… 메모리 보고 갈래?(3) Day #3. 이쯤 했으면 이제 그만 엔터치자 오늘부터 1일♡ 1) Debugging  (IDA, GDB 사용) 우리는 이미 tomato.c 소스가 있지만, 보통 소스는 잘 안주잖아여 없다

bpsecblog.wordpress.com

소스 코드가 없을 경우 IDA의 헥스레이 기능을 활용하면 다음과 같이 바이너리를 디컴파일 하여 원본 코드와 흡사한 소스를 보여준다. f5 단축키를 이용하여 사용할 수 있다. (헥스레이 기능이 매우 편리할 것 같아서 조만간 IDA나 헥스레이 기능이 있는 무료 툴인 기드라를 설치해야 겠다.)

IDA에서 8번째 라인을 보면 strcpy((char *) &v4, argv[1]); 코드에서 버퍼오버플로우 취약점이 있다. argv[1]을 v4 변수에 복사할 때 버퍼 크기를 검증하는 코드가 없기 때문이다. 변수 v4와 v5는 지역변수 이기 때문에 스택에 할당되고, length가 검증되지 않은 v4에 argv[1]의 값이 복사될 때 v4는 할당받은 공간 그 이상을 사용하게 되고, v5가 v4보다 높은 주소에 할당되어 있다면 v5에 원하는 값을 삽입할 수 있다. strcpy는 버퍼오버플로우 취약점이 발생할 가능성이 있기 때문에 strcpy_s로 대체하여 사용하는것을 권고하고 있다.

main 함수의 어셈 코드는 다음과 같다.

strcpy 함수의 주소는 0x08048515이므로 이 위치에 breakpoint를 걸고 strcpy가 실행되기 전후의 스택을 확인할 것이다.

0xbffff022는 esp가 가리키는 값 즉, v4이고, 0xbffff2d7은 esp+0x4가 가리키는 값 즉 argv[1]이다. strcpy 호출 전에는 argv[1]에만 값이 들어 있다가 strcpy로 v4에도 값이 들어간 것을 확인할 수 있다.

다음은 main의 어셈코드 일부이다. 

cmp는 비교하는 명령으로 첫번째 값에서 두번째 값을 뺄 때 마이너스가 되면 CF=1, 0이면 ZF=1 플래그가 설정된다. 플래그는 CPU의 플래그 레지스터에 저장되는 처리 데이터 이다. 플래그의 설정에 따라 분기문 조건이 다르다. 아래 jne의 경우 jump not equal로 비교 결과가 다르면 main+108 주소로 점프한다. 이 주소로 점프할 경우 system 함수를 실행하지 않는다. 따라서 ebp-0xc의 값이 0x1 이여야 system 함수를 실행하는 것이다.

현재 스택의 구조는 다음과 같다.

v4의 값이 흘러 넘치면 v5에도 영향을 미칠수 있다.

v4의 범위가 벗어나서 이후 메모리에 42(B)가 들어간 것을 확인할 수 있다.

v4의 값을 흘러넘치게 해서 v5가 0x00000001이 되면 셸을 띄울 수 있을것이다.
B로 채워진 부분들이 0x00000001이 되면 조건문은 일치시켜서 셸을 띄울 수 있을 것 같다.

다음과 같이 넣으면 gdb로 확인했을 때 다음과 같다.

메모리에 들어가는 값을 보면 0x00000001이 아닌 다른 값이 들어가는 것을 확인할 수 있다. 문자열 00000001이 들어가는 것이다. hex값을 메모리에 넣으려면 다음과 같이 python 이나 perl 같은 스크립트 언어를 사용하여 넣어야 한다.

python -c 옵션을 사용하면 command 라인에서 출력할 수 있고, 커맨드의 출력값은 `(백틱)을 이용해서 넘길 수 있다. tomato 파일의 인자로 백틱으로 감싼 파이썬 명령을 넣으면 v5 변수에 0x00000001을 덮어쓸 수 있다.

파이썬 명령어가 tomato 파일의 인자로 들어가서 v4 변수를 덮어쓰고 v5 도 0x00000001로 채워서 셸이 따지는 것을 확인할 수 있다.

 

해당 글의 원문은 아래 주소에서 확인할 수 있다.

bpsecblog.wordpress.com/2016/04/04/gdb_memory_2/

 

gdb를 사용하기 위해 다음의 코드를 컴파일하여 사용한다.

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
//tomato.c
#include <string.h>
#include <stdio.h>
 
void func2() {
    puts("func2()");
}
 
void sum(int a, int b) {
    printf("sum : %d\n", a+b);
    func2();
}
 
int main(int argc, char *argv[]) {
 
    int num=0;
    char arr[10];
 
    sum(1,2);
    strcpy(arr,argv[1]);
    printf("arr: %s\n", arr);
    if(num==1){
        system("/bin/sh");
    }
    return 0;
}
cs

이 소스코드에서 sum함수는 코드 내부에서 a + b의 결과값을 출력해주는 함수를 새로 만든 것이고, strcpy() 함수는 문자열을 다른 배열이나 포인터(메모리)로 복사하는 함수이다. system함수는 인수로 실행시킬 프로세스의 이름을 받아 그 프로세스를 실행하는 것이다.

 

이 소스코드는 다음 명령으로 컴파일 한다.

gcc -fno-stack-protector -o tomato tomato.c

-fno-stack-protector 옵션을 사용하는 이유는  gcc는 스택을 보호하기 위해 'canary' 라는 것을 삽입하여 함수 내에서 사용하는 스택 프레임과 return address 사이에 canary를 넣는다. 만약 Buffer Overflow가 발생하여 canary를 덮으면 이를 감지하고 프로그램을 강제 종료 한다. 이 과정을 SSP(Stack Smashing Protection)이라고 한다. 해당 옵션을 사용라게 되면 오버플로우가 발생해도 프로그램이 강제 종료 되지 않도록 보호기법을 해제한 것이다.

반대로 모든 프로시저에 이 보호기법을 적용하려면 -fstack-protector-all 옵션을 사용하면 된다.

 

gdb는 오픈소스로 공개되어 있는 무료 디버거로 코드에서 어떤 라인을 실행할 때 어떤 값이 어떤 메모리 주소에 올라가는지 과정을 보여주는 것이다. 

다음과 같은 명령어로 gdb로 프로그램을 실행할 수 있다.

gdb ./tomato

gdb로 tomato를 실행하면 다음과 같은 화면이 나온다.

보기 편하도록 어셈코드를 인텔 형식으로 설정하려면 다음 명령어를 사용한다.

set disassembly-flavor intel

at&t 형식으로 보려면 intel 자리에 att를 넣으면 된다.

disas main으로 main함수의 어셈블리 코드를 확인하면 다음과 같다.

b는 breakpoint를 거는 명령으로 b *[메모리주소]의 형태로 사용한다. 메모리 주소에는 메모리 주소, 함수의 이름, 이를 기준으로 한 offset <+0> 으로 걸어도 된다.

모두 같은 주소에 breakpoint가 걸림을 확인할 수 있다.

 

info b 명령으로 breakpoint 정보를 확인할 수 있다.

breakpoint가 중복되니 삭제한다.

삭제할때는 d (breakpoint 번호) 의 형태로 삭제한다.

프로그램을 실행할 때는 run 명령을 사용하는데 뒤에 매개변수를 넣을 수 있다. 

위와 같은 실행은 터미널에서 ./tomato aaaaaaaaaa로 실행한 것과 동일하다.

main 함수의 시작 위치에 breakpoint를 걸었기 때문에 main 함수를 확인해 보면  => 화살표로 main에 멈춰있는 것을 확인할 수 있다.

ni 명령을 통해 인스트럭션을 한줄 한줄 실행할 수 있다.

ni를 몇번 더 해서 원문과 비슷한 위치에 eip가 멈추도록 했다.

현재 멈춘곳 이전의 인스트럭션을 보면 esp 값을 ebp 값에 저장하고 있다. esp, ebp는 스택에 관련된 레지스터 이다.

 

그 메모리에 어떤 값이 담겨있는지 확인하기 전에 메모리 출력 방식에 대해 정리하겠다.

몇바이트 만큼, 몇 진법으로 출력할 지 옵션을 주어 출력할 수 있다.

해당 메모리 주소의 값을 각각 1바이트, 2바이트, 4바이트 만큼 출력한 것이다. x/ 뒤에 b는 1바이트, h는 2바이트, w는 4바이트를 의미한다.

위는 해당 메모리 주소 값을 각각 16진수 10진수로 출력한 것이다. 

다음과 같이 두 바이트 옵션과 진법 옵션을 같이 사용할 수 있다.

위를 보면 4바이트 출력한 것과 1바이트씩 4개를 출력한 것이 순서가 반대이다. Intel CPU는 바이트를 배열할 때 거꾸로 써서 예를 들어 0x12345678을 저장한다고 하면 0x78563412처럼 거꾸로 저장하는 것이며 이를 Little Endian 이라 지칭한다.

gdb는 디버깅시 보기 편하게 하기 위해 Big Endian 형식으로 0x12345678와 같이 출력하기 때문에 1바이트씩 출력할 때와 4바이트로 출력할 때 달리보인다.

 

특정 레지스터를 기준으로 메모리 값을 보고 싶으면 다음과 같이 $register_name로 출력할 수 있다.

info reg 또는 i r 명령을 사용하면 레지스터 정보를 출력할 수 있다.

 

스택과 관련있는 레지스터의 종류는 다음과 같다.

  • EBP : 스택의 가장 밑바닥 주소를 가진 레지스터
  • ESP : 스택의 가장 상단 주소를 가진 레지스터
  • EIP : 현재 실행중인 명령의 주소를 가진 레지스터

현재 EIP의 상황은 화살표가 가리키고 있고, 그 전에 실행된 push ebp 와 mov ebp, esp라는 두 줄의 어셈 코드가 의미하는 것은 스택(프레임)이 생성된다는 것이다. 저 두 줄의 의미는 함수가 실행이 될때 그 이전의 ebp(sfp)를 스택에 push 하고, 현재 esp를 ebp에 저장하라(새로운 ebp 생성)는 뜻이다. push는 스택 포인터를 하나씩 증가시키는 어셈 코드이다.

 

이제 sum 함수에 breakpoint를 걸고 실행시킨 후  스택의 상황을 볼것이다. breakpoint까지 한번에 실행하려면 c명령을 사용하면 된다.

sum의 함수 위치에 정확히 멈춰있는 것을 확인할 수 있다.

sum 함수가 call 되기 전의 상황은 다음과 같다.

sum 함수의 주소는 0x80484b4 이기 때문에 이 위치에 breakpoint를 걸고 함수 안으로 들어가면 스택 esp(0x00000001)위에 다음 인스트럭션의 주소가 쌓일 것이다.

이 함수의 어셈 코드를 확인해 보면 다음과 같다.

이 함수도 마찬가지로 push ebp, mov ebp,esp로 시작하고 있다. push ebp를 실행하면 그 이전 스택프레임(main)의 ebp 가 sum의 스택프레임에 push 되서 main의 ebp 0xbffff038이 esp에 들어갈 것이다. 

그리고 mov ebp, esp를 실행하면 현재 esp를 sum stack frame의 바닥 즉, sum의 ebp가 되서 sum의 ebp에는 main의 ebp가 있게 될 것이다. 

상단 첫번째 줄의 상태는 다음과 같다.

새 스택프레임의 ebp를 기준으로 return address는 ebp-4, 매개인자는 ebp-8, ebp-12에 들어가게 된다.

다음은 sum의 어셈 코드이다.

sum함수 안에서 func2 함수를 호출하고 있다. func 함수 주소에 breakpoint를 걸고 실행한 모습은 다음과 같다.

스택의 모습은 func2 stack frame, sum stack frame, main stack frame 가 차례로 있을 것이다. 

구간별로 나눠보면 다음과 같다.

현재 eip의 상황은 다음과 같다.

leave 실행 전의 스택 상황은 다음과 같다.

leave 를 실행한 후 스택의 상황은 다음과 같다.

위에서 빨간색으로 표시해둔 func2의 stack frame이 정리되며 ebp가 sum의 ebp로 바뀌고 esp도 sum의 esp로 바뀐다. 그리고 ret를 만나면 0x080484d8로 리턴해서 나머지 sum의 인스트럭션을 실행하고, sum의 스택프레임을 정리하고 나머지 main의 인스트럭션도 실행한 후 프로그램은 종료된다. 

해당 글의 원문은 아래 링크에서 확인할 수 있다.

bpsecblog.wordpress.com/2016/03/08/gdb_memory_1/

 

우리집에 GDB 있는데… 메모리 보고갈래? (1)

들어가기에 앞서… 컴(존)알못들이여 오라. 보안 공부하다 높다란 진입장벽 앞에서 좌절하신 분들께 이 글을 바칩니다. 해커가 되겠다는 청운을 안고 대학을 입학했으나.. 4년째 뉴비..★ 컴알못

bpsecblog.wordpress.com

4GB 메모리의 구조는 다음과 같다.

4GB는 위의 계산 과정을 거치면 2^32 byte 사이즈의 메모리라는 것을 알 수 있다. 

1 byte는 주소 공간 한 개의 크기 이기 때문에 4GB는 2^32개의 주소를 가질 수 있다.

1bit는 0 과 1 의 값만 가지기 때문에 32bit는 2^32만큼의 숫자를 표현할 수 있다. 

메모리는 주소로 접근하는데 32비트 운영체제는 수 표현을 2^32까지 밖에 못하기 때문에 8GB 이상의 램을 가지고 있어도 접근할 수 없다.

따라서 4GB 메모리 주소의 범위는 0x00000000 ~ 0xFFFFFFFF 까지 인 것이다.

 

시스템 운영에 필요한 메모리, 운영체제는 커널 영역에 올라가 있다. 사용자가 운영체제가 올라가 있는 커널영역에 마음대로 접근할 수 있다면 시스템이 안정적으로 운용될 수 없기 때문에 사용자가 함부로 커널 영역에 접근할 수 없도록 메모리를 유저영역과 커널영역으로 나누어 사용하는 것이다. 나누는 크기는 운영체제마다 다르고, 설정에 따라 영역의 크기를 조정할 수 있다.

 

유저 메모리 영역의 구조는 다음과 같다.

먼저 메모리에 코드가 올라온다. C소스를 컴파일하면 어셈블리 코드로 바뀌는데 이 코드가 코드 영역에 올라가는 것이다. 주소는 32bit 시스템은 주로 0804~  로 시작되는 영역에 올라간다.

 

데이터 영역은 전역변수가 로드되는 영역으로 항상 동일한 메모리에 위치하게 된다. 만약 사용자가 입력하는 값이 전역변수로 정의되어 있으면 고정된 메모리 주소에 원하는 값을 쓸 수 있게 되어 익스플로잇에 활용될 가능성이 높아질 수 있다.

 

힙 영역은 동적메모리를 할당하여 사용하는 공간이다. 대표적인 메모리 할당 함수로는 malloc이 있다. malloc로 필요한 공간을 할당하면 힙 영역을 쓰고, 쓰다가 필요 없으면 free로 해제하면 된다. 이때 힙 영역에 어떤 값을 엄청 많이 할당하고 free해준후 이 공간을 재사용하면 익스플로잇에 활용될 수 있다. 이러한 기법을 UAF라 한다.

 

스택영역은 메모리의 가장 바닥부터 채워진다. 스택은 높은 주소에서 부터 거꾸로 자란다. 그래서 스택 영역의 주소는 보통 0xbfff 이런식이다.  스택에 계속 자라다가 커널 영역을 건드리면 안되기 때문이다. 스택 영역은 함수 인자나 지역변수들이 올라온다.

 

Lesson 8

우리가 만들 수 있는 파일에는 크게 두가지 종류가 있다.

일반 텍스트 파일은 글자로만 이루어진 파일이다. 이러한 텍스트 파일은 cat 명령어로 출력한다.

프로그램 소스 파일은 컴퓨터 언어로 입력한 파일이며 컴파일 이라는 과정을 거쳐 실행 가능하도록 만든 후 사용한다.

텍스트 파일은 다음과 같이 생성할 수 있다.

1. 쉘 프롬프트에서 cat > 파일이름.txt 라고 입력

2. 원하는 내용 작성

3. control + D키 입력

1번 과정에서 > 문자는 리다이렉션 이라고 부르며 cat의 결과를 모니터가 아닌 파일로 보내는 것이다.

리다이렉션을 한 번 사용하면 새로운 파일이 만들어지지만, 기존의 내용을 보존시키면서 글자를 추가하려면 다음과 같이 리다이렉션을 두 번 사용하면 된다.

cat >> 파일이름.txt

 

소스 파일을 만드는 방법도 텍스트 파일과 동일하지만 언어를 컴퓨터 언어인 C언어로 작성한다는 차이가 있다.

C언어로 소스 작성이 끝났으면 컴파일을 해야 프로그램을 실행할 수 있다.

컴파일은 다음과 같이 진행한다.

gcc -o 프로그램이름 소스파일이름 

프로그램을 실행할때는 만들어진 프로그램이 있는 경로에서 실행해야 한다. 내가 만든 프로그램의 이름은 program.exe 이기 때문에 /home/trainer8/program.exe 를 입력하면 프로그램이 실행된다. 또한 현재 디렉토리를 의미하는 . 을 이용하여 ./program.exe처럼 상대경로로 현재 디렉토리에 있다면 바로 실행할 수 있다.

 

Lesson 9

리눅스는 각 사용자마다 특별한 권한을 부여하여 그 권의 범위를 넘어서지 않는 파일들만 제어할 수 있도록 설정되어 있다. ls -al 명령으로 파일들의 권한에 대한 정보를 출력할 수 있다.

리눅스의 권한에는 4가지 종류의 사용자가 있다.

    • user(유저) : 아이디로 로그인 한 자기 자신을 의미한다.
    • group(그룹) : 모든 유저는 하나 이상의 그룹에 속하게 된다. 그 그룹을 따로 변경하지 않는 한 자신의 유저네임과 같은 이름의 그룹에 속하게 된다.
    • other(아더) : 유저와 그룹을 제외한 모든 다른 사람을 의미한다.
    • root(루트) : 절대적인 권한을 가진 사용자이다. 어떤 권한에도 구애받지 않고 파일들을 제어할 수 있다.

id 라는 명령을 사용하면 다음과 같이 출력된다.

uid는 User ID의 약자이다. id라는 명령을 통해 현재 로그인한 계정의 uid는 2009라는 것을 알 수 있다.

gid는 group ID의 약자이다. 다른 사람을 자신의 gid를 가진 그룹에 속하게 할 수 있고, 특별한일이 없는 한 gid는 항상 uid와 같다.

groups는 현재 자신이 어떤 그룹에 속해있는지 말해준다. 임의의 변경이 없으면 기본으로 uid와 같은 그룹에 속하게 된다.

위는 ls -al 로 test1 이라는 파일을 확인한 것이다. rwxrwxrwx를 보면 rwx rwx rwx 로 3문자씩 끊었을 때 각각 유저, 그룹, 아더의 권한을 의미한다. 영문자 r, w, x는 순서대로 읽기(r), 쓰기(w), 실행(x) 권한을 의미한다. 위 예시에서 trainer9 자리가 유저, trainer10 자리가 그룹을 의미한다. 이 둘을 제외한 모든 아이디는 아더이다.

따라서 위의 test1 파일은 trainer9 라는 uid를 가진사람과 tranier10이라는 gid를 가진 사용자, 이 둘을 제외한 다른 모든 사람들에게 읽기, 쓰기, 실행 권한이있다. 즉 아이디가 있는 모든 사용자는 이 파일을 마음대로 접근하고 변경할 수 있다.

위 test2 파일의 경우 guest라는 uid를 가진 사용자는 읽기, 쓰기, 실행 권한이 있고, trainer1 이라는 gid를 가진 사용자는 읽기 실행 권한이 있고, 이 둘을 제외한 다른 모든 사용자는 실행 권한만 갖는다.

 

읽기 권한은 cat 명령으로 파일을 볼 수 있는 권한이고, 쓰기 권한은 그 파일의 수정 권한이다. 실행권한은 윈도우에서 exe 파일처럼 실행할 수 있는 파일로 리눅스에서는 확장자로는 실행파일인지 확인할 수 없고, 권한에서 x라는 문자가 있는지 확인하고 판단해야 한다.

 

루트 권한만 접근할 수 있는 shadow 파일의 권한은 다음과 같다.

 

Lesson 10

해킹방식에는 Local 해킹과 Remote 해킹 두가지 종류의 방식이 있다.

Remote 해킹은 자신의 해킹하고자 하는 서버에 아이디가 없을 때 아이디를 얻고자 시도하는 것이다.

Local 해킹은 해킹 하고자 하는 서버에 일반 계정이 있을 때 관리자 권한을 얻고자 시도하는 것이다.

 

SetUID는 일시적으로 자신의 ID를 변경하는 것을 말한다. passwd 명령어를 사용하여 패스워드를 바꾸는 경우 shadow파일은 루트만 수정권한이 있는데 일반 사용자도 패스워드를 변경할 수 있다. 이러한 경우 passwd에 SetUID가 걸려 있어 passwd 파일을 실행하는 동안 루트로 일시적인 아이디 변경이 되는 것이다.

SetUID는 필요한 만큼만 관리자 권한을 제공하고 그 일이 끝나면 다시 가져가는 것이 일반적이지만 SetUID가 걸린 파일들을 통해서 관리자 권한을 완전히 가져오게 될수도 있다.

버퍼 오버플로우의 경우 루트 권한으로 SetUID가 걸려 있는 파일에 프로그램의 에러를 발생시키고 에러가 나는 순간 /bin/bash 파일을 실행하게 조작하여 프로그램이 실행되는 동안 루트 아이디로 변경되고, 그 상태에서 /bin/bash를 실행해 루트 권한의 쉘을 획득하는 공격이다.

 

다음과 같이 x권한이 올 자리에 s가 있으면 SetUID가 설정된 것이다. s는 x(실행권한)도 포함한다.

이 파일은 모든 사용자에게 실행권한이 있지만 root에 SetUID가 걸려있기 때문에 어느 사용자가 저 파일을 실행해도 루트의 권한을 갖는다.

find / -perm -4000 의 명령을 사용하면 서버 전체에서 SetUID가 걸린 모든 파일을 찾을 수 있다. 위 명령을 해석하면 / 에서부터 적어도 SetUID가 걸린 모든 파일을 찾으라는 명령이다. 4는 SetUID를 의미하며 000은 rwx모두를 의미한다.

find 명령의 옵션은 다음과 같다.

  • -perm : 권한과 일치하는 파일을 찾음
  • -name : 이름과 일치하는 파일을 찾음
  • -user : 유저와 일치하는 파일을 찾음
  • -group : 그룹과 일치하는 파일을 찾음

위 옵션들을 조합하여 사용할 수 도 있다. find / -user root -perm 4000은 루트 권한으로 SetUID가 걸린 파일을 찾는 것이다.

FTZ의 이후 레벨별 문제들은 다음 레벨로 SetUID가 걸린 파일을 찾아서 그 파일을 이용하여 그 레벨의 쉘을 얻으면 된다. 쉘을 얻고 my-pass를 입력하면 해당 계정의 패스워드를 확인할 수 있다.

Lesson 4

최 상위 디렉토리의 디렉토리 종류와 역할은 다음과 같다.

  • bin : 필수적인 리눅스 실행 파일들이 저장된 위치(ls, rm등)
  • boot : 리눅스 부팅 관련 파일, 커널 이 들어있는 곳
  • dev : 컴퓨터에 설치된 하드웨어 관련 정보들이 파일 형태로 저장되어 있는 위치
  • etc : 패스워드 파일, 쉐도우 파일, 리눅스 설정 파일 등이 들어 있는 위치
  • home : 일반 사용자들의 디렉터리가 들어가는 위치
  • lib : 라이브러리 파일들이 들어있는 위치
  • mnt : mount 명령을 사용하여 마운트 시킨 시디롬 등이 들어가는 위치
  • proc : 프로세스들이 파일 형태로 저장되는 위치
  • root : 루트 계정의 홈 디렉토리
  • sbin : 기본 명령을 제외한 시스템 관리용 실행파일이 들어있는 디렉토리
  • tmp : 임시로 파일을 저장하는 디렉토리, 권한 관계없이 누구나 파일을 생성할 수 있다.
  • usr : 다양한 응용 프로그램들이 설치되어 있는 디렉토리
  • var : 시스템 운영 중에 생성되는 각종 임시 파일들, 외부 접속에 대한 로그 파일들이 저장되는 위치

리눅스의 중요한 역할을 하는 파일들은 다음과 같다.

  • /etc/passwd : 사용자들에 대한 간단한 정보
  • /etc/shadow : 사용자들의 패스워드가 암호화하여 저장됨
  • /etc/services : 서버가 어떤 서비스를 하는 중인지 보여줌
  • /etc/issue.net : 처음 접속할 때 나오는 화면( FTZ의 경우 해커스쿨의 F.T.Z에 오신걸 환영합니다! 라는 문구)
  • /etc/motd : 로그인 후에 나오는 메시지
  • ~/public_html : 각 사용자들의 홈페이지 파일

다음은 FTZ의 퀴즈와 답이다.

 

 

Lesson 5

whoami : 접속한 자기 계정이 누구인지 확인하는 명령어

id: 접속한 계정에 대한 더 많은 정보

cat : 파일을 읽는 명령어

다음은 cat명령어로 /etc/passwd 파일을 읽은 것의 일부이다.

각 줄의 가장 왼쪽의 단어가 사용자들의 아이디 이다.

 

커널은 리눅스의 심장부 역할을 하는 파일로, 커널 버전에 따라 리눅스의 성능 차이가 발생할 수 있다. 같은 버전의 리눅스라도 커널 버전이 높다면 속도도 빨라지고, 안정성도 높아진다.

uname -a 명령으로 커널 버전을 확인할 수 있다.

위 FTZ의 경우 커널 버전은 2.4.20이고, 2.2.18이전 버전의 모든 커널들은 취약점을 가지고 있다고 한다.

root권한은 리눅스나 유닉스에서 모든 파일을 관리하는 절대적인 관리자 권한이다. 리눅스는 여러명의 사용자가 동시에 작업을 하기에 자신에게 권한이 부여된 파일만 제어할 수 있지만, 루트 권한을 가진 사용자는 어떠한 권한에도 구속되지 않고, 서버를 조정할 수 있다.

 

서버에 설치된 OS는 cat 명령어로 /etc/*release라고 입력하면 확인할 수 있다.

Red Hat Linux 9 버전임을 확인할 수 있다.

 

패키지의 정보는 rpm -qa명령으로 확인할 수 있다.

위는 명령 실행 결과의 일부로 많은 패키지들이 깔려 있음을 확인할 수 있다.

 

cat명령어로 /proc/cpuinfo를 확인하면 서버의 cpu정보를 확인할 수 있다.

 

Lesson 6

/etc/passwd(이하 패스워드 파일)은 한 서버를 사용하는 사용자들의 모든 정보를 기록해 놓은 파일이다. 사용자의 아이디, 어떤 함호를 이용하여 로그인을하는지, 속한 그룹, 이름 등의 정보가 들어있다.

다음은 패스워드 파일의 root 계정이 있는 줄을 가져온 것이다.

: 은 필드를 구분해주는 문자이다.

첫 번째 필드는 로그인 할 때 사용되는 아이디이다. 두번째 필드는 원래 패스워드가 적혀있는 부분인데 리눅스 버전  6.0 이후 크래커들이 악용을 막기 위해 두번째 필드의 패스워드를 없애버리고 따로 모아 저장하는 /etc/shadow 라는 파일을 사용한다. /etc/shadow파일은 일반 사용자 계정들은 접근할 수 없다. 

세 번째 필드는 컴퓨터가 사용자를  인식하기 위한 숫자이다. 0은 root를 의미한다. 네번째 필드는 사용자가 속해있는 그룹을 말해준다. 

다섯번째 필드는 사용자 이름을 말해주는 부분이다. 위의 경우 관리자를 의미하는 Admin이 사용자 이름이다. 여섯 번째 필드는 해당 사용자가 로그인 성공했을 때 위치하게 되는 홈 디렉토리 이다. 7번째 필드는 사용자가 처음으로 로그인 했을 때 실행되게 할 프로그램이다. 위 처럼 로그인 했을 때 쉘이 실행되도록 하는 것이 일반적이다.

Lesson 7

리눅스는 서버의 용도로 사용되기 때문에 데이터들의 손실을 막기 위해 백업을 필수적으로 해야 하며 백업할 때는 압축 명령어 들이 사용된다.

tar은 여러개의 파일을 하나로 합치는 명령어이다. 옵션은 다음과 같다.

  • c - Create : 새로운 파일을 만듬
  • x : eXtract : 압축 해제
  • v : View : 압축이 되거나 풀리는 과정을 출력
  • f : File : 파일로서 백업을 하겠다는 옵션

따라서 파일을 합칠때는 cvf, 해제할 때는 xvf 를 사용한다.

위 명령은 현재 디렉토리의 모든 파일(*)을 songs.tar 이라는 이름으로 합친 것이다.

실제로 다음과 같은 파일이 생성됨을 확인할 수 있다.

위 만들어진 파일의 용량 부분을 보면 3개의 파일을 뭉쳤는데 용량은 오히려 더 증가하였다. tar은 압축하지 않고, 파일을 합쳐버리기만 할 때 사용된다. 또한 gzip은 한 번에 한개의 파일만 압축할 수 있기 때문에 여러개의 파일을 압축하려면 tar로 파일을 합친 뒤 압축해야 한다.

gzip을 사용하여 tar로 합친 파일을 압축하면 다음과 같이 용량이 줄은것을 확인할 수 있다.

tar과 gzip을 사용한 파일들의 확장자는 다음과 같다.

  • tar : tar 프로그램을 사용하여 하나로 합쳐진 파일
  • gz : gzip 프로그램을 사용하여 압축된 파일
  • tar.gz : tar프로그램을 사용하여 합친 후 gzip을 사용하여 압축한 파일
  • tgz : 위 확장자를 합쳐서 tgz로 사용할 때도 있다.

현재 디렉토리에는 다음과 같이 압축된 파일만 존재한다.

gzip에서 -d 옵션을 사용하면 gzip 압축을 해제할 수 있다.

tar에 xvf를 사용하여 tar을 해제할 수 있다.

 

Lesson 1

해커스쿨 서버는 현재 열려있지 않아서 로컬에 ftz 서버를 구축하고, ssh를 이용하여 접속했다. 

비밀번호를 입력하여 로그인 성공시 프롬프트가 나타나며 프롬프트는 명령 입력 대기 상태에서 명령을 기다린다.

프롬프트에는 접속 id, 서버, 현재위치의 정보가 있다.

프롬프트에 ls 명령어를 입력하면 다음과 같이 현재 디렉터리의 파일 목록을 확인할 수 있다.

ls 명령어에 -l 옵션을 추가하여 ls -l 을 입력하면 어떤것이 디렉터리이고, 어떤것이 파일인지 등 더 자세한 정보를 확인할 수 있다.

각 파일마다 구분되어 있는 필드에는 파일이 생성된 날짜 정보가 있고, 14698 같은 숫자들은 파일의 용량이다. tranier1 trainer1 과 같은 내용을 퍼미션에 관련한 정보이며 맨 첫번째 필드가 파일의 모드와 성격 정보를 가지고 있는데 가장 왼쪽의 문자가 - 면 일반 파일이고, d이면 디렉터리를 의미한다.

이후로 퀴즈가 나오는데 퀴즈 내용과 답은 다음과 같다.

ls명령어로는 보이지 않는 숨김 파일은 -a 옵션을 추가한 ls -a 명령어로 확인할 수 있다.

숨김 파일은 파일명 가장 앞에 . 이 붙어있다는 특징있다. 

ls 명령어는 ls -al 처럼 둘 이상의 옵션도 함께 사용할 수 있다. 함께 사용하면 사용하는 모든 옵션의 효과를 한번에 사용할 수 있다.

 

Lesson 2

자신이 현재 속해있는 디렉터리를 확인하려면 pwd 명령어를 이용하면 된다.

프롬프트에서 현재 위치의 정보는 앞의 경로는 생략하고 가장 뒤쪽의 경로만 나타내고 있다.

현재 경로에서 한 단계 위로 가려면 cd .. 명령어를 이용한다. 지금 경로에서 한단계 위로 가면 /home 가 되고 또 한단계 위로 가면 / 가 되는데, / 은 최상위 디렉토리이며 루트 디렉토리 라고도 한다.

경로를 내려갈때는 cd trainer2 처럼 디렉토리 이름을 붙이면 된다. 

.. 을 사용하지 않고 한번에 루트 디렉터리로 올라가려면 cd / 을 입력하면 된다. 또한 한 번에 trainer2 로 내려가려면 cd /home/trainer2처럼 루트 디렉토리부터 모든 경로를 입력하면 된다.

새로운 디렉토리를 만드는 명령어는 mkdir 이다. mkdir에 만들 디렉토리의 이름을 지정해주면 된다.

mkdir linuxer로 linuexer로 디렉토리를 만들면 ls 명령어로 확인 시 만든 디렉토리가 생성됨을 확인할 수 있다.

 

디렉토리를 삭제할 때는 rmdir이라는 명령어를 이용한다. 

rmdir linuxer을 입력하고 ls -al을 입력하면 linuxer 디렉터리가 삭제됨을 확인할 수 있다.

 

cp 명령어는 파일을 복사하는 명령어다. cp 파일1 파일2 와 같은 형태로 사용하면 파일1이 파일2의 이름으로 복사된다.

 

파일을 삭제하는 명령어는 rm이다. rm 복사사본을 입력하고 ls 명령어로 확인하면 파일이 삭제됨을 확인할 수 있다.

 

파일을 옮기는 명령어는 mv이다. mv명령어는 파일을 옮기는 것을 파일을 복사하고 원본 파일을 자동으로 삭제하는 것으로 처리하기 때문에 파일 이름을 변경할 때도 자주 사용된다.

mv 파일1 파일 2의 형식으로 사용하면 파일1의 이름이 파일2로 변경된다.

 

Lesson3

터미널은 ssh나 텔넷등의 서버를 통해 접속한 것이고, 콘솔은 부팅한 컴퓨터에 직접 접속한 것이라는 의미이다. 

 

w명령어를 사용하면 현재 서버에 접속한 사용자 목록을 확인할 수 있다.

첫번째 필드는 접속한 아이디를 나타낸다. 두번째 필드는 콘솔로 접속했는지, 터미널로 접속했는지 보여준다. tty면 콘솔접속, pts면 터미널 접속을 의미한다. 3번째 필드는 접속한 사람의 ip를 의미한다. 콘솔 접속한 경우 ip는 보이지 않는다. 4번째 필드는 로그인 시간이고, 5번째 필드는 지연시간으로 사용자가 얼마나 아무것도 입력을 안했는지 확인할 수 있다. 6번째 필드는 cpu를 사용한 지연 시간이고, 7번째 필드는 what 필드에 있는 명령이 지연된 시간, 8번째 필드는 현재 사용하고 있는 명령을 보여준다.

 

finger -l 명령어를 확인하면 사용자에 대한 좀 더 많은 정보를 확인할 수 있다.

 

tty 명령어를 사용하면 자신의 터미널 정보를 확인할 수 있다.

 

ifconfig 명령어를 사용하면 ip 정보를 확인할 수 있다.

 

write 사용자id /dev/pts/pts번호 의 명령어로 서버에 접속한 다른 사용자에게 메시지를 전달할 수 있다.

 

wall "내용" 의 명령어로 현재 접속한 모든 사용자에게 메시지를 전달할 수 있다.

0부터 9까지 몇번 사용됬는지 저장할 공간을 int형 포인터에 int 자료형 크기로 10개 할당하였다.

자연수 세개를 입력받아, 입력받은 자연수 3개를 곱한다.

곱한 값이 0보다 클때까지 반복하고 반복문 안에는 곱한 값을 10으로 나눈 나머지를 배열의 인덱스로 하여 그 숫자가 나올때 마다 해당 인덱스에 1씩 누적하고 곱한 값을 10으로 나눈다.

할당한 메모리를 해제하고 종료한다.

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int a, b, c, mul, i;
    int *numPtr = (int*)malloc(sizeof(int* 10);
    scanf("%d %d %d"&a, &b, &c);
    mul = a * b * c;
    
    while(mul > 0)
    {
        i = mul % 10;
        numPtr[i] = numPtr[i] + 1;
        mul /= 10;
    }
    
    for(int i=0; i<10; i++
    {
        printf("%d\n", numPtr[i]);
    }
    free(numPtr);
    return 0;
}
cs

+ Recent posts