Hacker News new | ask | show | jobs
by simiones 19 days ago
> Array memory is on the stack.

Array memory can sit on either the stack or the heap.

> The size of that array is actually not known at run time, its only known at compile time, where any reference to that length gets resolved by the compiled.

This is also a bit misleading, in two ways. First, it's not clear what you mean by "size" here - the size of the memory block(s), or the shape of the array?

Second, many people think that the C runtime doesn't know the amount of memory allocated to an array, but this is actually false. It's just the C abstract model that for some reason chose to not expose this information - but the size is actually always stored and accessible, and this is virtually mandated by the standard: otherwise, `free(arr)` couldn't realistically work, it would have to be `free(arr, size)`. This is one of the weirdest inefficiencies of C, in fact - it requires you to store the size of arrays twice - once in user code, and another time in the internal logic of the allocator.

Edit: and as a fun extra, C++ not only inherited this mistake from C, but reproduced it again, meaning that a C++ array allocated with new[] actually stores the size twice, at least with typical implementations - once in the C++ runtime and again in the allocator - and still requires the user-space code to store it a third time. This is because `delete[]` needs to call the destructors of all of the elements of the array, regardless of where and how the array was allocated, so the number of array elements needs to be stored alongside the object itself.

2 comments

>Array memory can sit on either the stack or the heap.

No, if we are using the definition of an array that is like int c[] = ..., that is always going to be on the stack. Heap continuous memory =/= array. You can use the [] operator to access it like an array, but fundamentally, as far as structures in C language are concerned, those 2 are different, because they get treated by compiler differently.

>but the size is actually always stored and accessible, and this is virtually mandated by the standard: otherwise, `free(arr)` couldn't realistically work,

That would only be true if each element in the array was a char.

The dynamic data structure stores total amount of memory allocated by address, it has no info about the size of the element, so it can't infer the actual number of items at runtime. You could write your own malloc that does this, but generally, that is left to the user for flexibility. For example, a really good practice in C coding that basically solves any double free is a mempool that allocates all the memory up front. That way, you never really even have to call free, and the memory you allocate can be partitioned any way you chose dynamically.

> that is always going to be on the stack.

Unless your C implementation doesn't use a stack for data storage. Which existed, you know: IIRC the C compilers for Cray machines used linked lists to hold activation frames. And of course, there are PIC microcontrollers where you can't really use the hardware stack for anything except return addresses.

> int c[] = ..., that is always going to be on the stack

Why? In the following code, only c will be allocated on the stack:

  int a[]={1,2,3};
  foo() {
    static int b[]={1,2,3};
    int c[]={1,2,3};
  }
The point is anything that is not dynamically allocated has size known at compile time.
> No, if we are using the definition of an array that is like int c[] = ..., that is always going to be on the stack. Heap continuous memory =/= array. You can use the [] operator to access it like an array, but fundamentally, as far as structures in C language are concerned, those 2 are different, because they get treated by compiler differently.

Well, not necessarily. For one thing, if we have a function foo(int c[]), it's debatable if c is an array variable or a pointer variable. However, what's not debatable is that you can allocate a struct on the heap, and that struct can have an array member - e.g. `struct foo { int a[10]; }; [...] struct foo *x = malloc(sizeof(struct foo));` would allocate an array on the heap as part of the struct.

> That would only be true if each element in the array was a char.

That's why I said that it depends on what exactly you mean by the size of the array. It's also true that in today's world at least, malloc() will often allocate more memory than you actually ask for, to optimize against fragmentation - and then the internally stored size is the size of the actual allocation, not the logical size that you requested - which may not even fit into a whole number of array elements. So, I was being a little overly simplistic (lying) for dramatic effect.

> For example, a really good practice in C coding that basically solves any double free is a mempool that allocates all the memory up front.

While this is a very valid technique for certain purposes, especially when dynamic allocation is needed in very high performance code, it's very much not a valid solution for memory safety - quite the contrary, it's a terrible practice for that. In particular, this is almost exactly the issue that caused the infamous HeartBleed vulnerability in OpenSSL to stay hidden for so long: the use of a memory pool for the buffers used to store TLS packets was hiding the buffer overflow from UBSan and valgrind and similar tools, since the reads were perfectly valid from a language perspective (they were never reading from free()d/unallocated memory, only from memory that had been released to the memory pool).

Its all about what the compiler sees.

Structs are a defined type, which means its construction (and therefore total size) has to be known , the array definition with size is necessarily part of that struct type. So anytime that struct is used, the compiler needs to see its definition, and thus can safely infer the size. Thats pretty much the whole reason structs are a thing - the very basic type that allows you to pass around data format during the compilation process.

Arrays are not defined as types in C, they are really at most just syntax convenience. So if another function takes an array as a parameter, and it gets compiled as part of a file, there is no way for the compiler to auto infer what would get passed into it.

Char allocation usually involves +1 bytes for null terminated strings, which is used as a signal for allocated memory. So strlen(char *) is accurate.

>quite the contrary, it's a terrible practice for that. In particular, this is almost exactly the issue that caused the infamous HeartBleed vulnerability in OpenSSL to stay hidden for so long: the use of a memory pool for the buffers used to store TLS packets

The heartbleed vulnerability was not due to mempool. It was due to a combination of lack of bounds checking, and not zeroing out the memory containing secure keys when its deallocated. Even if it didn't use mempool, leaks would still be possible.

Even for char*, it's very possible that malloc() will store more memory than strictly required. But you're right, `char x[] = "abc"` will require a minimum of 4 bytes wherever x gets allocated (stack or global segment).

> The heartbleed vulnerability was not due to mempool. It was due to a combination of lack of bounds checking, and not zeroing out the memory containing secure keys when its deallocated. Even if it didn't use mempool, leaks would still be possible.

I didn't say that the bug was caused by the mempool, I said that the bug was very hard to find by regular tools such as valgrind and UBSan because it used mempools instead of regular allocations - so that all of the logical out of bounds accesses were not actually UB nor were they accessing unallocated memory, which those tools could have caught.

> Second, many people think that the C runtime doesn't know the amount of memory allocated to an array, but this is actually false. It's just the C abstract model that for some reason chose to not expose this information.

There are some counterpoints:

1) Conceptually, allocated memory block and data structure / array in it are not related. You can allocate memory block and then subdivide it to multiple different structures / arrays. You can implement sub-allocators.

2) Heap allocator does not need to store exact length of allocated object. For example, it could have several fixed-length slab allocators for smaller objects, select matching one during malloc() and use address range to find slab during free().

3) Array can be also on the stack (VLA or alloca()).

4) Arrays can be also on memory allocated outside of C library allocator (e.g. mmap()).

All are fair points, I was being a bit cavalier with the facts. I'll also add that many if not all modern malloc() implementations actually allocate somewhat larger amounts of memory than your request, to respect various alignment requirements and/or to avoid excessive fragmentation - even when not using pure slab allocations.

I do think the C++ bookkeeping from new[]/delete[] however has few if any similar caveats - the runtime really needs exactly the kind of information you also need in your code; the only caveat I can imagine is that it might omit this information for types that don't need destruction, such as `int`, but I don't know if this is a plausible optimization in realistic use cases that are not trivial.