데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      Memory-safe in C

      joykim 24.03.07
      1,642 11 0
      DEVOTEE 요약
      C와 C++ 사용 중단을 주장하는 논란에 대해 토론하면서, 프로그래밍 작업이 결국 가용한 자원을 가져오고, 사용하고, 반환하는 것이라고 쓴 글이다. 여기서 메모리 관리가 중요하다고 강조하며, 각 프로그래밍 언어별로 메모리 관리 방식이 다르지만 결국 메모리라는 자원을 가져오고(할당), 반환하는 과정을 거친다. C/C++에서는 성능을 중요시하기 때문에 메모리를 복사하기보다 포인터를 전달하는 구조로 작성하는데, 이로 인해 메모리 할당과 반환이 문제가 될 수 있다고 주장한다. 이에 대해, GLib의 Automatic Cleanup 매크로가 해결책을 제시하고 있음을 소개하였다. 이를 통해 메모리 할당과 반환의 고민에서 어느정도 해방될 수 있으나, 메모리를 전달할 때 주의해야 한다고 덧붙였다.
      DEVOTEE 추천 블로그

      근래 갑작스럽게, 백악관, ‘C’와 ‘C++’ 사용 중단 촉구라는 글이 노출이 되어 C-er 로서 그렇지 않아요라고 주장하기 위해서 글을 써보았습니다.


      메모리 관리

      어떠한 프로그램을 작성하든 결국 프로그래머가하는 일은 가용한 자원을 가져오고, 사용하고, 반납하는 작업을 반복해서 하는 것입니다.

      언젠가부터 최근까지 {Front, Backend} 개발자라는 용어가 통용되고 있는 듯하고, 각 역할에 따라 각기 다른 철학을 가진 프로그래밍 언어를 사용하는 것으로 세분화 되어 있는 듯하여,

      여기서 언급하는 “자원”에 대해서 쉽게 동의하지 않을 수도 있습니다.

      하지만, 간단한 문자열 하나만 변수에 할당하더라도 우리는 “메모리”라는 자원을 할당하고 사용하는 것입니다.

      프로그래밍 언어에 따라서는 반납에 대해 가비지 컬렉터(Garbage Collector)에 그 수행을 위임하여 편리함을 제공하고 있기는 하나, 모든 절차는 이 흐름에서 벗어나지 않게 됩니다.

      여러가지 자원 중에서 메모리는 프로그램의 성능과 안전에 관련된 가장 중요한 자원이기 때문에, 별도로 메모리 관리라는 주제로 각 프로그래밍 언어별 많은 서적을 찾아 볼 수 있습니다.

      당연한 말이지만, 굳이 구분하자면 메모리를 가져오고(할당), 반납하는 과정은 안정성과 관련이 있으며 성능은 메모리의 사용과 관련이 있다고 할 수 있습니다.


      이를 설명하기 위해 임의의 문자열을 생성하는 generate_random_string() 함수와

      주어진 문자열에서 숫자와 알파벳이 몇개씩 있는지 찾아보는 count_alnum() 함수를 만들어 보았습니다.

      Function generate_random_string(length):
          Define characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
          Define random_string = ''
      
          For i from 1 to length:
              Generate a random index from 0 to the length of characters - 1
              Append the character at the random index in characters to random_string
      
          Return random_string
      
      Function count_alnum(input_string):
          Define count_digits = 0
          Define count_letters = 0
      
          For each character in input_string:
              If character is a digit:
                  Increment count_digits by 1
              Else if character is an alphabet letter:
                  Increment count_letters by 1
      
          Return count_digits, count_letters

      이 두 함수를 사용하는 방법은 아마도 다음과 같이 진행 될 것입니다.

      # Step 1: Generate a random string
      length_of_random_string = 10  # Arbitrary length
      random_string = generate_random_string(length_of_random_string)
      print("Generated random string:", random_string)
      
      # Step 2: Count numbers and alphabets in the generated string
      digits_count, alphabets_count = count_alnum(random_string)
      print("Digits count:", digits_count)
      print("Alphabets count:", alphabets_count)

      문자열은 generate_random_string() 함수내에서 생성되고, random_string으로 전달 됩니다.

      이 random_string은 다시 count_alnum() 함수에 전달되어 {digits,alphabets}_count로 결과값을 반환 하도록 되어있습니다.

      명시적으로 나타나지는 않았지만, random_string의 역할은 count_alnum()이 연산한 뒤에는 더이상 필요가 없기에 반환되어야하는 것이 마땅하나,

      이 pseudo code가 가비지 컬렉터를 가지고 있어서 반환되었을 것이라고 생각하는 것이 편리할 듯합니다.

      다만 여기서 말하고 싶은 것은, 위 문장에서 전달이라고 표현한 부분입니다.

      생성한 random_string을 전달할 때, 새로운 메모리 영역으로 복사를 할 것인지, 아니면 random_string이 저장된 장소(포인터)를 알려줄지에 따라 사용하는 메모리의 양뿐만 아니라,

      메모리 관리 (할당, 반환) 절차의 필요 여부가 결정되기 때문에 이는 성능과 연결이 될 수 밖에 없습니다.

      문제는 C/C++로 작성하는 코드들은 대게 성능을 중요시하다보니, 메모리를 복사하기 보다는, 포인터를 전달하는 구조로 작성하게 됩니다.

      전달 횟수가 거듭될수록, 그리고 한번에 처리해아는 할당된 메모리의 수가 많을수록,

      관리하기는 어려워지며 이로 인하여 할당과 반환이 적절하기 이루어지지 않아 결국 Memory-Safe 문제를 불러오게 된다고 볼 수 있습니다.


      이제 우리가 집중해야할 문제는 무엇인지 간단해졌습니다.

      할당된 메모리의 사용 방법은 프로그램마다, 알고리즘마다, 심지어 프로그래머의 개인 선호도에 따라 달라질 수 있습니다.

      하지만 명확한 것은 할당한 것은 반드시 반환해야한다는 것이고, 다행히도 이를 위한 해결책은 GLib의 Automatic Cleanup 매크로가 제시를 하고 있습니다.

      C에서는 말이죠.


      예외 처리를 포함한 메모리 할당 및 반환

      C에서는 기본적으로 malloc()과 free()를 통해 메모리를 관리합니다.

      그러나 이러한 primitive function은 내부적으로는 유저 영역에서 커널 영역으로 호출을 하는 것이고, 커널이 직접 응답하는 구조로 커널의 사정에 따라 반드시 성공한다는 보장이 없습니다.

      실패한 경우에는 커널에서 알려준 에러코드를 확인해야하는 절차가 필요하며, 예외 상황에 대해 적절하게 대응하는 코드는 다음과 같습니다.

      #include <stdio.h>
      #include <stdlib.h>
      
      void* safe_malloc(size_t size) {
          void *ptr = malloc(size);
          if (ptr == NULL) {
              // Handle the memory allocation failure
              fprintf(stderr, "Error: Memory allocation failed for %zu bytes.\n", size);
              exit(EXIT_FAILURE); // Exit the program or handle the error as per your requirement
          }
          return ptr;
      }
      
      void safe_free(void **ptr) {
          if (ptr != NULL && *ptr != NULL) {
              free(*ptr);
              *ptr = NULL; // Prevents double-free errors
          }
      }
      
      int main() {
          int *numbers = safe_malloc(10 * sizeof(*numbers)); // Allocate memory for 10 integers
          
          // Use the memory...
          for(int i = 0; i < 10; i++) {
              numbers[i] = i;
          }
          
          // When done, free the memory safely
          safe_free((void**)&numbers);
          
          // After safe_free, numbers is NULL and cannot be accidentally freed again
          if(numbers == NULL) {
              printf("Memory successfully freed and pointer is NULL.\n");
          }
      
          return 0;
      }

      기본적으로 메모리 할당에 실패한 것과 NULL에 대해 접근하는 경우를 방지한 것이기 때문에

      위의 코드는 malloc()과 free()를 직접사용하는 것보다는 안전하다고 할수는 있습니다.

      하지만 한가지 메모리 사용에 대해서는 그 효율을 장담할 수 없습니다. 반복적인 메모리 할당과 반환은 메모리를 조각으로 만들게 되고(fragmentation),

      이는 결국 충분히 사용가능한 메모리가 있음에도 요청한 크기만큼의 연속된 공간이 존재하지 않아, 실제 할당에는 실패하는 상황이 발생하게 됩니다.

      메모리 할당이 실패한 경우에는 위 코드에 기술한 내용에 따르면 프로그램을 바로 종료하게 되어있고 이는 메모리 관리에 실패한 것이나 다름없는 결과가 나오게 됩니다.


      메모리 할당 및 반환 with GLIB

      GNOME 프로젝트에서는 여러가지 경우를 상정하여 고안한 GLib를 제공하고 있습니다.

      이는 기존 C에서 제공하는 시스템 호출에 대한 안성성과 효율성을 확보하기 위한 기능과 여러 유용한 자료구조를 제공하고 있으며,

      거의 모든 운영체제에 호환이 되도록 작성되어있습니다.

      메모리 할당과 반환의 관점에서만 본다면, 다음과 같은 예제로부터 시작할 수 있습니다.

      #include <glib.h>
      #include <stdio.h>
      
      int main() {
          // Allocate memory for 10 integers using GLib's g_new
          // Syntax: g_new(Type, Number_of_Elements)
          gint *numbers = g_new(gint, 10);
      
          // Use the allocated memory
          for (int i = 0; i < 10; i++) {
              numbers[i] = i * 10; // Example operation
              printf("numbers[%d] = %d\n", i, numbers[i]);
          }
      
          // Free the allocated memory using GLib's g_free
          g_free(numbers);
      
          // After g_free, it's a good practice to set the pointer to NULL to avoid accidental use
          numbers = NULL;
      
          return 0;
      }

      여전히 메모리의 할당과 반환은 프로그래머의 몫입니다만,

      다음 예제는 좀더 진보된 형태의 자료구조와 매크로를 통해 마치 가비지 콜렉터가 있는 듯한 형태로 사용하는 것을 볼 수 있습니다.

      #include <glib.h>
      #include <stdio.h>
      
      // Automatic cleanup for GArray
      G_DEFINE_AUTO_CLEANUP_FREE_FUNC(GArray, g_array_unref, NULL)
      
      int main() {
          // Allocate a GArray with automatic cleanup
          g_autoptr(GArray) numbers = g_array_new(FALSE, FALSE, sizeof(gint));
          
          // Use the allocated array
          for (int i = 0; i < 10; i++) {
              gint num = i * 10; // Example operation
              g_array_append_val(numbers, num);
              printf("numbers[%d] = %d\n", i, g_array_index(numbers, gint, i));
          }
      
          // No need to manually free the array; it will be automatically freed
          // when it goes out of scope (i.e., at the end of the main function)
          
          return 0;
      }

      G_DEFINE_AUTO_CLEANUP_FREE_FUNC 매크로는 메모리 할당에 사용할 자료구조에 대해 반환할 때 사용하는 함수를 정의합니다.

      그러면 해당 자원에 대해 참조점이 없을때 자동으로 여기서 선언한 반환용 함수가 호출 되게 됩니다.

      메모리 할당시에 C에서는 포인터를 사용하나, 포인터에 대한 참조점 추적을 위해 g_autoptr 매크로를 통해서 포인터를 선언합니다.

      그러면 이제 더이상 할당된 메모리에 대한 반환 시점을 고민하지 않아도 됩니다.


      실제 프로젝트에서 사용 예

      GLib의 자동화된 메모리 관리 매크로들은 가비지 컬렉터가 있는 언어인 양 어느정도 메모리 할당과 반환의 고민에서 해방되게끔 도와줍니다.

      다만 이렇게 할당 메모리를 전달할때에 주의해야하는데, g_autoptr을 선언한 포인터는 블럭을 벗어나는 시점에서 반환 함수가 호출되기 때문에

      블럭이 종료되는 시점에서 해당 포인터의 소유권을 g_steal_pointer를 통해 넘겨주는(말그대로 훔치는) 작업이 필요합니다.


      다음은 이러한 매크로들을 공격적으로 사용했던 황새울 프로젝트의 코드의 일부 입니다.

      ///
      /// https://github.com/hwangsaeul/gaeguli/blob/110822e039888b2fcc8a400e6fd20a88dd956fa2/gaeguli/pipeline.c#L498
      ///
      GaeguliPipeline *
      gaeguli_pipeline_new (GVariant * attributes)
      {
        g_autoptr (GaeguliPipeline) pipeline = NULL;
        GaeguliVideoSource source;
        const gchar *device = NULL;
        GaeguliVideoResolution resolution;
        guint framerate = 0;
        GVariantDict attr;
      
        g_return_val_if_fail (g_variant_is_of_type (attributes,
                G_VARIANT_TYPE_VARDICT), NULL);
      
        g_variant_dict_init (&attr, attributes);
        g_variant_dict_lookup (&attr, "source", "i", &source);
        g_variant_dict_lookup (&attr, "device", "s", &device);
        g_variant_dict_lookup (&attr, "resolution", "i", &resolution);
        g_variant_dict_lookup (&attr, "framerate", "u", &framerate);
      
        g_debug ("source: [%d / %s]", source, device);
        pipeline = g_object_new (GAEGULI_TYPE_PIPELINE, "source", source, "device",
            device, "resolution", resolution, "framerate", framerate, "attributes",
            g_variant_dict_end (&attr), NULL);
      
        return g_steal_pointer (&pipeline);
      }
      
      ...
      
      ///
      /// https://github.com/hwangsaeul/gaeguli/blob/110822e039888b2fcc8a400e6fd20a88dd956fa2/tests/test-pipeline.c#L74C1-L103C2
      /// 
      static void
      test_gaeguli_pipeline_instance (TestFixture * fixture, gconstpointer unused)
      {
        GaeguliTarget *target;
        g_autoptr (GaeguliPipeline) pipeline =
            gaeguli_pipeline_new_full (GAEGULI_VIDEO_SOURCE_VIDEOTESTSRC, NULL,
            GAEGULI_VIDEO_RESOLUTION_640X480, 30);
        g_autoptr (GError) error = NULL;
      
        g_signal_connect (pipeline, "stream-started", G_CALLBACK (_stream_started_cb),
            fixture);
        g_signal_connect (pipeline, "stream-stopped", G_CALLBACK (_stream_stopped_cb),
            fixture);
      
        target = gaeguli_pipeline_add_srt_target_full (pipeline,
            GAEGULI_VIDEO_CODEC_H264_X264, GAEGULI_VIDEO_STREAM_TYPE_MPEG_TS, 2048000,
            "srt://127.0.0.1:1111", NULL, &error);
      
        g_assert_no_error (error);
        g_assert_nonnull (target);
        g_assert_cmpuint (target->id, !=, 0);
        fixture->target = target;
      
        gaeguli_target_start (target, &error);
        g_assert_no_error (error);
      
        g_main_loop_run (fixture->loop);
      
        gaeguli_pipeline_stop (pipeline);
      }

      위 코드에서 주의깊게 보실 부분은 GaeguliPipeline 포인터입니다.

      gaeguli_pipeline_new()의 내부에서 할당한 메모리를 외부 함수에서 불러와서 사용할때 포인터의 소유권을 넘긴 부분과

      할당한 메모리의 반환을 위해 별다른 노력을 기울이지 않은 것을 볼 수 있습니다.


      맺음말

      우리는 답을 찾을 것이다. 늘 그랬듯이.


      원문링크: Memory-safe in C

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      joykim 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기