a strange but powerful interview question

  Рет қаралды 263,921

Low Level Learning

Low Level Learning

4 ай бұрын

Which way does the stack grow? What even IS a stack? In this video I'll talk about an interview question that still haunts me to this day.
🏫 COURSES 🏫 Learn to code in C at lowlevel.academy
📰 NEWSLETTER 📰 Sign up for our newsletter at mailchi.mp/lowlevel/the-low-down
🛒 GREAT BOOKS FOR THE LOWEST LEVEL🛒
Blue Fox: Arm Assembly Internals and Reverse Engineering: amzn.to/4394t87
Practical Reverse Engineering: x86, x64, ARM, Windows Kernel, Reversing Tools, and Obfuscation : amzn.to/3C1z4sk
Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software : amzn.to/3C1daFy
The Ghidra Book: The Definitive Guide: amzn.to/3WC2Vkg
🔥🔥🔥 SOCIALS 🔥🔥🔥
Low Level Merch!: lowlevel.store/
Follow me on Twitter: / lowleveltweets
Follow me on Twitch: / lowlevellearning
Join me on Discord!: / discord

Пікірлер: 1 000
@embeddedbastler6406
@embeddedbastler6406 4 ай бұрын
Technically, we also make the assumtion that the architecture even *has* a stack. The C standard does not require a stack to be present at all. In fact the word "stack" is not even metioned in the C standard one single time. Instead the C standard requires the architecture to have some kind of mechanism to automatically allocate local variables. But this does not have to be a stack.
@dasten123
@dasten123 4 ай бұрын
But doesn't the fact that you can do recursion automatically mean that there is a stack?
@PTh0rn
@PTh0rn 4 ай бұрын
@@dasten123 no, frames could be allocated on the heap and freed automatically on return. some virtual machines do this iirc
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
@@CallousCoder But it is not something that the C language requires. The C standard makes no assumptions about which target platform and what compiler will be used. E.g. I can compile my code with compiler options to randomize order of things that are usually allocated on the stack. Or even have a system which uses two or more stacks for data of different size and type. A well-formed program (without UB) without pieces relying on implementation-specific behavior will still be running correctly. Any solution to the question of the video is bound to rely on assumptions about implementation-specific behavior. I.e. such a solution will not be fully governed by what the C standard dictates about correct programs written in C.
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
@@PTh0rn Hardware may even have multiple separate stacks for e.g. code and data. It is not so exotic: x86's CET extension maintains a separate isolated stack for return addresses. If one has multiple stacks, then these stacks may theoretically grow in different directions, and the question of the video becomes invalid.
@CallousCoder
@CallousCoder 4 ай бұрын
@@GrigoryRechistovYes I totally agree! It is more dictated by the ABI of the OS and even in a lesser extend by the hardware -- the hardware can help, but there's no requirement to use it. The OS ABI is more decisive in this.
@OMGclueless
@OMGclueless 4 ай бұрын
All of your solutions use undefined behavior. The compiler is free to do whatever it wants because pointer comparison operators between objects that are not from the same array are undefined behavior.
@Gregorius421
@Gregorius421 3 ай бұрын
That's probably the reason why the mistakenly inverted condition still returned the expected result 😀
@AndrewLenharth
@AndrewLenharth 3 ай бұрын
Not only are they all undefined behavior, the attempted use of volatile was incorrect and gcc and clang remove the recursion and the code just winds up comparing two locations in the same stack frame anyway.
@npip99
@npip99 3 ай бұрын
How do you check which way the stack grows then? Obviously we assume the stack grows in a particular direction. C doesn't require that at all.
@OMGclueless
@OMGclueless 3 ай бұрын
@@npip99 It's possible to avoid the UB by casting the pointers to intptr_t before comparing them. Then the best we can say that *if* the compiler has not inlined the function call and *if* the target ABI uses a stack and *if* the stack grows monotonically in one direction, then the function will return the right answer.
@Gregorius421
@Gregorius421 3 ай бұрын
​@@npip99Alternatives: 1. No need for recursion, use another function and add `___attribute___ ((noinline))` to avoid inlining. Works with gcc/clang. 2. Turn off optimization with `gcc -O0` (dash, capital O, zero). This also stops the compiler from reversing the order of local variables (as it happened in the video). 3. I'd use just one function and `alloca(10000)` (it's like malloc, but on the stack) because gcc does not change the order of alloca() -s: #include #include #include int main() { intptr_t a1 = (intptr_t)alloca(30000); intptr_t a2 = (intptr_t)alloca(20000); printf("a2-a1: %d ", a2-a1); } About undefined behavior (UB): Although the standard says to avoid UB, the reality of life is that we encounter UB regularly (the foundation of the C standard was designed in a different era, before we had much experience with portability and what works). Avoiding it is not always feasible, but mostly it just goes unnoticed. Each compiler has their own (maybe different) solution when implementing UB, so we can't assume what the compiler will do, but have to dig deep and find out. Different compilers will need different solutions to solve this challenge. Of course this effort often stops at the phase "it works on my computer" 😃
@andreys7944
@andreys7944 4 ай бұрын
the C standard (ISO/IEC 9899) does not explicitly mandate the use of a stack for local variables. The standard describes the abstract machine, and the choice of using a stack or another method for managing local variables is left to the implementation.
@ante646
@ante646 4 ай бұрын
no idea what the best solution is here - my naive guess is to ask about the ABI (specifically calling conventions), write a function with many parameters in the signature until they are no longer passed via register, then add another two parameters such that those two values are guaranteed to be passed by being pushed onto the stack, subtract their addresses (operand order dependant on calling convention), then return a value based on the sign of the subtraction result - abysmal style but ugly and correct is better than stylish and wrong right? (could honestly be ugly and wrong for all I know) edit: this is naive af considering access to ABI would contain the answer, down below someone suggested examining stack pointer directly via asm which is stylish and correct
@asedtf
@asedtf 4 ай бұрын
I don't remember, but do you even have to have a stack at all?
@quazar-omega
@quazar-omega 4 ай бұрын
Academic C be like: the compiler implementation is left as an exercise to the reader
@cyrilemeka6987
@cyrilemeka6987 4 ай бұрын
​@quazar-omega 😂
@darrennew8211
@darrennew8211 4 ай бұрын
@@ante646 It's UB to compare pointers from two different allocations.
@Double-Negative
@Double-Negative 4 ай бұрын
I think the result you have there is still not quite enough. The compiler could try to reason about the function call and realize that since it'll always terminate within 2 recursive steps, that it can be inlined into itself, then this inlined version of the function can reorder the addresses of its local variables. The solution to this depends on the compiler. On gcc, you can set -O0 to prevent inlining entirely, or you could put __attribute__ ((noinline)) into the function signature to make sure it's actually allocating stack space.
@shadamethyst1258
@shadamethyst1258 4 ай бұрын
You could also mark other as volatile, so the compiler cannot make that assumption anymore
@EnterOne100
@EnterOne100 4 ай бұрын
It can. Inline, the reorder other allocations arbitrarily. The just make sure to perform the operations one after the other.
@shadamethyst1258
@shadamethyst1258 4 ай бұрын
@@EnterOne100 How can it inline, since the compiler no longer can prove that the recursion is finite? For all it knows, the volatile pointer could be set back to NULL between each recursive calls
@ciano5475
@ciano5475 4 ай бұрын
Maybe we can check the position of the return address with __builtin_frame_address(0)
@coolcax99
@coolcax99 4 ай бұрын
@@shadamethyst1258 or you can take the variables from input
@mrphlip
@mrphlip 4 ай бұрын
Comparing pointers that point to things that aren't part of the same object (ie two entries in the same array, or two members of the same struct) is undefined behaviour. The C compiler can make this code do whatever it wants. The spec reads: When two pointers are compared, the result depends on the relative locations in the address space of the objects pointed to. If two pointers to object types both point to the same object, or both point one past the last element of the same array object, they compare equal. If the objects pointed to are members of the same aggregate object, pointers to structure members declared later compare greater than pointers to members declared earlier in the structure, and pointers to array elements with larger subscript values compare greater than pointers to elements of the same array with lower subscript values. All pointers to members of the same union object compare equal. If the expression P points to an element of an array object and the expression Q points to the last element of the same array object, the pointer expression Q+1 compares greater than P. In all other cases, the behavior is undefined.
@shroomer3867
@shroomer3867 2 ай бұрын
I practiced this myself after seeing that error being pointed out in the comments. It doesn't even require recursion. 1) Declare a function where you will have a volatile two size array, this is done in order to invoke everything on the stack in C. Place two volatile int values with them inside of it. 2) Since you declared the array you know which one goes above and below so you can just compare the two locations of the array (&array[0] > &array[1]) which would be defined behaviour. 3) Do the return done in the video based on the difference of the two addresses and you're golden.
@KingBobXVI
@KingBobXVI Ай бұрын
​@@shroomer3867 - this was my first thought but with structs... but actually I don't think it would work. Since the elements always have to go in ascending order, the function would always return "up", regardless of whether the stack grows up or down. The stack growing down doesn't mean the actual data goes backwards (or changes endianness). This would only affect whether the head or tail of the array is closer to the base of the stack.
@tunafllsh
@tunafllsh 4 ай бұрын
I don't have much practical knowledge in C, but It's good to know what I've learnt in my university was enough to come up with the same reliable solution to this problem.
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
This is not a reliable solution. A compiler may still optimize away this recursive call because it can prove that the depth of recursion will always be two levels. So there is no guarantee that desired number of stack frames will be created. Not to mention that neither the presence of stack nor regular allocation strategy for placing objects on it are guaranteed at all.
@plesleron
@plesleron 4 ай бұрын
@@GrigoryRechistov wouldn't the creation of stack frames (and thus their order in memory in this example) count as observable behavior that the compiler couldn't optimize away?
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
​@@plesleron No, for at least two reasons. 1. No guarantees exist that a stack frame will be created for a given function call. Inlined functions have no own stack frame, and the compiler is free to inline any function (not just those marked with "inline" keyword). Recursive functions are not exempt from it. A compiler may partially inline recursive call or specializations of the function. E.g. I know for a fact that GCC will replace tail recursive calls with jumps when the conditions are right. All such tail-called functions will share the same stack frame. 2. You cannot access (neither read nor write) stack frames from a C program in a portable way. Their existence is not observable. No standard library call is defined for accessing them, and relying on a third-party library or inline assembly or such is outside the well defined portable behavior (i.e. it is not UB, thankfully, but the result is unspecified or implementation-defined).
@tunafllsh
@tunafllsh 4 ай бұрын
@@GrigoryRechistov What if you make it do recursion based on a rng value. Surely a compiler couldn't unroll this.
@Tawnos_
@Tawnos_ 4 ай бұрын
@@GrigoryRechistov I would see that as the continuing part of the interview, where you discuss that this makes assumptions about compiling without optimizations, why those assumptions are necessary (your comment about deterministic optimization), and the depth of testing required/importance of getting the right answer to the problem every time versus the vast majority of the time. All of these engineering tradeoffs are important outside of the toy problem.
@wChris_
@wChris_ 4 ай бұрын
The recusive version wouldnt work either, since the compiler could perform tail call optimization.
@almightyhydra
@almightyhydra 4 ай бұрын
Right, far simpler to use two functions and just disable compiler optimisations.
@CallousCoder
@CallousCoder 4 ай бұрын
@@almightyhydra even then you make the assumption that the function implementation uses a stack. C does not define that. So C would need to find a way to implement this without a stack when a CPU doesn't have a stack register or the running system doesn't have that concept.
@martinzihlmann822
@martinzihlmann822 4 ай бұрын
and far more explicit, but i understand, it's not good content for a video.
@reductor_
@reductor_ 4 ай бұрын
the compiler cannot tail call optimize as the lifetime of the argument still needs to be maintained but it could inline it, in the end the code is bad an has undefined behavior you cannot compare two unrelated pointers.
4 ай бұрын
Only, if it’s actually a tail call.
@iTakethingsapart
@iTakethingsapart 4 ай бұрын
I'm not confident that after optimizations, there's any defined behavior that accurately describes the stack direction, especially since the stack is usually just an implementation detail so the compiler is free to not use one at all.
@robonator2945
@robonator2945 4 ай бұрын
yeah that's one thing I'm not sure about here either. My first thought was to use a static array and compare &array[0] against &array[1] but the more I think about it a compiler could just sorta decide to work backwards for arrays compared to everything else, and it could decide to do *_anything_* backwards compared to everything else, so I'm not sure there is any architecturally-constant answer here.
@Stormrage476
@Stormrage476 4 ай бұрын
You can always write inline assembly where you push two values onto the stack and compare their addresses. That way, you only have to trust the compiler not to optimize your assembly (which I assume it wont do), and the assembler to correctly assemble your code. You can also do it in only assembly as well.
@kodicraft
@kodicraft 4 ай бұрын
@@Stormrage476 At that point, if you know which CPU architecture you're targeting, you might as well just check out the documentation and write code expecting the stack to behave in the way it's documented. In fact, I'd argue the most practical solution to this problem is just having a check at compile-time of the target architecture and compare it to a known database since that comes with no runtime overhead at all for what's effectively a compile-time constant.
@VivekYadav-ds8oz
@VivekYadav-ds8oz 4 ай бұрын
@@robonator2945 Pretty sure arrays are laid out from lower to higher address, simply because the way to access them uses this assumption. C has no way of knowing to "pretend" when *(arr + i) should actually be *(arr - i) in cases of arrays because arrays frequently decay as pointers. Hence it MUST allocate them in the typical fashion.
@TranscendentBen
@TranscendentBen 4 ай бұрын
Oh, that sounds like a nightmare. You're assuming a particular target processor, or you're writing assembly for every possible processor and using lines like #ifdef PROCESSOR_6502 to assemble the right code for the target processor. And my understanding of the problem was to do it "in C."
@denischen8196
@denischen8196 4 ай бұрын
You can use inline assembly to see if ESP increases or decreases when you use the push instruction.
@aspuzling
@aspuzling 4 ай бұрын
I think this is the only correct answer. In other words, "No I can't write a C program to determine the underlying architecture but I can write one in assembly".
@Double-Negative
@Double-Negative 4 ай бұрын
you also need to already know the architecture to write assembly
@adamsfusion
@adamsfusion 4 ай бұрын
That's putting the cart before the horse. In order to know what your stack pointer is to even reference it, you'd have to know the architecture.
@aspuzling
@aspuzling 4 ай бұрын
Exactly
@gregorymorse8423
@gregorymorse8423 4 ай бұрын
Lol if you know the machine language, you could also go to the Intel manual for x86 and see push eax pseudo code of [ESP]
@obiwanjacobi
@obiwanjacobi 4 ай бұрын
In the 90's I wrote a function that could tell if a pointer was stack allocated but I solved it with inline assembly - just getting the SP register and comparing it to passed in pointer.
@IamusTheFox
@IamusTheFox 2 ай бұрын
I actually really like that solution.
@Delfigamer1
@Delfigamer1 3 ай бұрын
1. "int x, y = 0" only initializes 'y'. To initialize both variables, you have to write "int x=0, y=0". 2. 'Volatile' does not just mean "don't optimize", its semantics are actually quite narrow - writes and reads from volatile objects are considered as observable side effects, so the compiler has to preserve their order among themselves and relative to other I/O. Note that only actual reads and writes matter - if you allocate a 'volatile' variable, but never actually access its contents, then the compiler is allowed to remove it in its entirety. So, in your example, the 'volatile' qualifier only affects the initialization "y = 0", while 'volatile' on 'x' is literally useless. Also, as other comments noted, even the recursive function is still not safe. Firstly, a modern compiler is able to inline even recursive functions (so that you'd have many layers spelled out at once), which would bring both locals into the same scope and allow their reordering. Secondly, even without that - since all inputs to the function are known at compile time, the compiler may just interpret it right away on its own virtual machine, which might be completely different to the host, including the stack growth direction (this is also one of the reasons why you can't do reinterpret_cast in constexpr). So, to make sure your code actually gets run on the host, and the way you wrote it, you should also disable compiler optimizations.
@FinBoyXD
@FinBoyXD 4 ай бұрын
Ok, I don't exactly understand how the stack is pictured here. So in the first example, if the address of y is greater than of x, then in this example it's growing down. But if the y is allocated later, then why does this mean then it's growing down (ie more negative) if the latter variable address was higher? But if you take the recursive example it makes sense, since x is now the variable that's allocated later, because we are at the end of the recursion and "other" is the previously allocated variable (and it's now right side of the greater than sign). So this makes sense to me, and seemingly disagrees with the first example. Yet their print was the same.
@Wail0s
@Wail0s Ай бұрын
This is the comment i was searching, in fact , if we suppose that x is pushed into the stack before y , then &x is greater than &y , therefore upordown() should return true.
@ghostphreek7301
@ghostphreek7301 4 ай бұрын
I am a bit confused on the first approach? if the pointer to x is greater then y then wouldn't that mean the stack is growing down and getting more negative? Since the second value that was allocated on the stack, y, has a smaller address then x which was the first value allocated on the stack? Assuming no compiler optimizations are made.
@AI-vy6ky
@AI-vy6ky 4 ай бұрын
I thought the same thing. Am I missing something obvious?
@00jknight
@00jknight 4 ай бұрын
Agreed. I'm pretty sure his first example is not only incorrect, but he misspoke multiple times when talking about it.
@noomade
@noomade 4 ай бұрын
@@00jknight LOL, I thought I was going mad... now i am totally confused because of course you could be the one that is wrong since you agree with a noob like me :P
@Gregorius421
@Gregorius421 3 ай бұрын
You're right. The condition is mistakenly inverted, that is wrong. So why is the result as expected? The compiler allocated the variables in a different order than defined, or did some other optimization that fooled the logic. This is the danger of undefined behavior. Funny that the mistake masks the compiler's unexpected behavior, without the presenter's knowledge. In any case the challenge achieved its goal, we now know quite some about him: did not verify the logic (no testing, even mentally), wrote if () return true; else return false; instead of return (); Rookie mistakes.
@noomade
@noomade 3 ай бұрын
@@Gregorius421 can you write a correct solution (as in one that you would write if you were FORCED to write SOMETHING in an interview)
@vytah
@vytah 4 ай бұрын
Both solutions invoke undefined behavior: you are not allowed to compare unrelated pointers for which is greater, only for equality. A compiler is free to optimize those comparisons to an arbitrary constant, irrespective of the stack growing direction.
@erikkonstas
@erikkonstas 4 ай бұрын
LOL, we're already within UB territory with that problem statement alone (which assumes the existence of a stack), so...
@vytah
@vytah 4 ай бұрын
@@erikkonstas Yes, but knowing it is undefined behavior and not merely implementation-defined behavior means we cannot rely on the solution even if we know there is a stack and there is nothing complicated with pointer representation, as the compiler is free to do whatever. Someone commented under the video that the result depends on optimization settings on x86_64, one of the better behaved targets. Someone else in the comments posted a better solution which, while still technically invoking undefined behavior, at least survives optimizations, as it uses volatile function pointers, and it would work reliably under a few reasonably common conditions.
@erikkonstas
@erikkonstas 4 ай бұрын
@@vytah Yeah I agree, I'm saying the interviewer is asking a bit of an iffy question here.
@noomade
@noomade 4 ай бұрын
@@vytah where is that better solution?
@vytah
@vytah 3 ай бұрын
@@noomade Here is it, by @dekutree64 volatile int *ap, *bp; bool f1(){*ap=*bp=1; return bp>ap;} bool(*volatile fp1)()=f1; bool f2(){int b=0; bp=&b; return (*fp1)();} bool(*volatile fp2)()=f2; bool up(){int a=0; ap=&a; return (*fp2)();} The conditions I think this relies on are 1. the stack exists 2. both return addresses and local variables are stored on that stack 3. all pointers are comparable 4. for two separate allocations, either all pointers into one are less than all pointers into the other, or greater 5. C implements pointer comparison the simplest way 6. sizeof(size_t) == sizeof(uintptr_t) 7. compiler always respects volatile for globals. Maybe few more.
@samsthomas
@samsthomas 4 ай бұрын
Love your videos. I like to think I’m a semi-competent C hacker on a good day, but it’s amazing how much I’ve forgotten or don’t consciously think about regularly since college.
@williamsloan7857
@williamsloan7857 4 ай бұрын
A few questions about your final solution: 1. Shouldn’t x still still be volatile? 2. Are you worried about if the compiler decides to do some form of optimization and removes the recursive call? 3. Why not get the stack pointer and compare it to the previous stack pointer?
@vytah
@vytah 4 ай бұрын
You cannot get the stack pointer (or anything resembling it) in C. You need to drop to assembly, which means you already know how your stack works.
@shipweck6253
@shipweck6253 4 ай бұрын
@@vytah you can write inline assembly in C. Though this kinda defeats the purpose of writing it in C.
@solhsa
@solhsa 4 ай бұрын
1) doesn't matter, the solution is still wrong. 2) and it's wrong because of this. 3) there's no stack pointer (or requirement for existence of stack) in C
@valizeth4073
@valizeth4073 3 ай бұрын
@@shipweck6253 Not really given that the task is "impossible" in C. If the interviewer asked me such a question I would assume it's a trick question.
@tonysofla
@tonysofla 4 ай бұрын
Most embedded C compilers would use Registers, not the Stack for temporary variables. Recursive probably would force it to use the stack as the compiler don't know how much space it needs. Side note, A Contant Int would force it to store it as ram variable, if you want it to be remembered for next call.
@anar.bastanov
@anar.bastanov 4 ай бұрын
Recursion doesn't necessarily force variables to be allocated on the stack, taking their address with & operator does (because values on registers don't have addresses)
@sorek__
@sorek__ 4 ай бұрын
Wouldn't constant be most likely kept in flash though? Especially for some embedded architectures. Another way to make sure would be to create compile size array to go around registers being used. But I think volatile would stop it from happening and also calling for address of variable wouldn't make it use registers (I think).
@tonysofla
@tonysofla 4 ай бұрын
@@anar.bastanov As return address is always store on the stack, would the test allow inline assemble? Have to make sure to use noinline for the function, and you would need to use correct asm for the platform. uint32_t sp; asm( "mov %%rsp, %0" : "=rm" ( sp )); return sp;
@tonysofla
@tonysofla 4 ай бұрын
@@sorek__ Using the keyword Constant inside a function is different. Passing an array of like 20 vars, should force the use of the stack. And that is why passing a single pointer is betters, so you don't use 20 stack push/pop's
@natnial1
@natnial1 4 ай бұрын
You can just use a print statement to guarantee the use of a variable’s address cannot be optimized away, this way you get an actual automatic variable rather than a register.
@zaper2904
@zaper2904 4 ай бұрын
I personally would allocate a small array to force the compiler to allocate on the stack.
@Pi7on
@Pi7on 4 ай бұрын
Yeah that's seems like the simplest way to me too.
@aspuzling
@aspuzling 4 ай бұрын
Could you explain a bit more? Why is allocating an array different from allocating an int? Why does allocating a small array guarantee that it will be allocated on the stack? What do you mean by "small" exactly? Is there a threshold that means larger arrays get allocated to the heap instead and if so what is that threshold?
@zaper2904
@zaper2904 4 ай бұрын
@@aspuzling You can't store an array in registers and an array in C must be allocated sequentially in a continuous chunk of memory otherwise pointer arithmetic doesn't work. Small means just that I'd personally allocate maybe 100 numbers just in case but as far as I know even just 2 would be fine.
@ryanmcclue6867
@ryanmcclue6867 4 ай бұрын
@@aspuzling An int would be 32bits on 64bit machine. So, the compiler could easily put this in any available 64bit general purpose register. If you were to allocate an array of size say 8 ints 'int arr[8];' then it's not feasible to use registers to store. Regardless of size, would only go on heap if storing to an address returned with 'malloc()' and friends.
@myusernameislongerth
@myusernameislongerth 4 ай бұрын
​@@zaper2904if you take an address of an object, it cannot be in register only.
@Zeutomehr
@Zeutomehr 4 ай бұрын
my favourite video of yours in a while, really liked this one
@rogerdeutsch5883
@rogerdeutsch5883 4 ай бұрын
Fascinating question I had not thought about before. Fantastic & concise explanation with great code example. Subscribed!
@Peter_Schluss-Mit-Lustig
@Peter_Schluss-Mit-Lustig 4 ай бұрын
it kinda hurts to see you return a boolean through an if-statement
@LowLevelLearning
@LowLevelLearning 4 ай бұрын
I was explicit for the sake of the video but I understand what you’re saying :P
@69k_gold
@69k_gold 4 ай бұрын
You mean the non-ternary approach?
@arjuntt2604
@arjuntt2604 4 ай бұрын
Easily understandable code does add value
@spacey6960
@spacey6960 4 ай бұрын
@@burndly But would that code change to not use the if else? Wouldnt you need the ? : operator?
@batatanna
@batatanna 4 ай бұрын
Nop. You can just return the comparison. ​@@spacey6960
@m.hosseinmahmoodi
@m.hosseinmahmoodi 4 ай бұрын
I actually thought of an extra function that has a variable and returns the address of the variable to UpOrDown and UpOrDown compares it to the address of its own variable. Recursion removes the need to create an extra function but adds the need for a pointer to be passed instead.
@BittermanAndy
@BittermanAndy 4 ай бұрын
Yeah, recursion unnecessarily confuses this IMO.
@maexxx
@maexxx 3 ай бұрын
That's exactly how I approached the question: typedef volatile int stackvar_t; int comparestackpositions(stackvar_t *otherstackpointer) { stackvar_t thisstackvariable; return &thisstackvariable - otherstackpointer; } bool upordown(void) { stackvar_t stackvariable; if (comparestackpositions(&stackvariable) > 0) return true; return false; }
@sandeepvk
@sandeepvk 2 ай бұрын
I loved this solution. Thanks. I am new to C but your channel has helped me step up by several orders of magnitude. You make the topic very interesting.
@rosettaroberts8053
@rosettaroberts8053 4 ай бұрын
If you look at the C11 reference for relational operators, section 6.5.8, this pointer comparison invokes undefined behavior since the variables aren't part of the same array or struct. So, a compiler would be allowed to optimize your function to lie about which way the stack grows. I'm not sure if any compilers actually do optimize this comparison.
@blazingblast
@blazingblast 4 ай бұрын
Some compiler options optimize out using multiple stack frames for recursion, if possible
@ic6406
@ic6406 4 ай бұрын
Probably the compiler doesn't have rights to optimize this case because recursive call depends on values below the call
@OMGclueless
@OMGclueless 4 ай бұрын
​@@ic6406 There's no dependency on values below the call. `return upordown(&x)` can be easily tail-call-optimized into a simple jump because there is no additional work to do after the call returns and the return value of the callee is our return value.
@baranjan6969
@baranjan6969 4 ай бұрын
I love it! I would try to use inline asm and compare esp but recursion is honestly much better
@gregorymorse8423
@gregorymorse8423 4 ай бұрын
alloca is much better and uses no function calls
@ArielNMz
@ArielNMz 4 ай бұрын
Very cool stuff. I haven't touched C in about 10 years since I moved on to Python/JS/Web and I'm glad to know I could've gotten almost 80% of that in an actual interview, assuming I knew it was a C interview and had time to do a quick refresher, I owe it all to my CS101 teacher back in high school.
@ProjectKneepads
@ProjectKneepads 4 ай бұрын
The recursive solution is very clever! I hadn't considered compiler optimization. Thanks for sharing!
@DesyncX
@DesyncX 4 ай бұрын
I was thinking in the same direction of multiple function stack frames. I was thinking of 2 functions: 1 basic upordown() function with no parameters which returns enum UP or DOWN. This upordown would simply call with the address of a variable in upordown another function checkIfStackIncreasesUp(int *reference_address) which returns bool by comparing the parameter received address with some internal variable's address. No recursion is needed :)
@Leto2ndAtreides
@Leto2ndAtreides 4 ай бұрын
Probably better to not name it upordown because it's returning a bool. I'm legit inclined to name it doesStackGrowUp... And I've never used does in a function name before. lol Your teammates should ideally not have to read the code of the function to understand what it's doing. And comments are not an adequate substitute for good naming.
@Gregorius421
@Gregorius421 3 ай бұрын
Use `___attribute___ ((noinline))` to avoid inlining. No need for recursion. Alternatively `clang -O0` disables optimizations like inlining.
@Gregorius421
@Gregorius421 3 ай бұрын
@@Leto2ndAtreides Yeah, the name was not really catchy. I thought about isUpOrNo() would get more interest.
@d4y92
@d4y92 4 ай бұрын
I'd probably solve that problem using "alloca()", which allocates memory in the stack and return its as a pointer
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
A single call to alloca() is not enough. It will return you a pointer to (allegedly) data on stack, yes. But then you need to compare it to another pointer allocated on the same stack. And this is where no guarantees are provided by the language about the order in which multiple alloca()'s or any other local allocations will happen.
@Henrix1998
@Henrix1998 4 ай бұрын
​​​@@GrigoryRechistovmaking second alloca conditional based on the first would guarantee the order int *first, *second; first=alloca(1); if (first) second=alloca(1); return first>second;
@rb1471
@rb1471 4 ай бұрын
I think it'd be slow in comparison
@Catterjeeo
@Catterjeeo 4 ай бұрын
I doubt the speed is all that much of a factor here. @@rb1471
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
@@Henrix1998 alloca() never returns NULL, so in this case the conditional may be optimized away as always true. Then the allocations may be reversed.
@henriksundt7148
@henriksundt7148 4 ай бұрын
2:17 Security people will comment that int x, y = 0; only initializes y with 0, not x. It should be int x = 0, y = 0; One more issue is that with comma separated variable initialization, C does not specify the order that the initialization has to take place. Thus, to check the stack direction, it should be int x; int y;
@noomade
@noomade 4 ай бұрын
still no guarantee that x is initialized first though right??
@henriksundt7148
@henriksundt7148 4 ай бұрын
@@noomade Even though in practice, most compilers initialize left-right, it is not defined as part of the C standard. E.g. if you do int x=0, y=x; you get a compiler error.
@henriksundt7148
@henriksundt7148 4 ай бұрын
@@noomade Statements separated by semicolon are executed sequentially, thus int x=0; int y=0; is guaranteed to initialize x first. I think you are right that only declaring them by int x; int y; without initialization does not impose a specific order on the stack, it may be up to the compiler.
@noomade
@noomade 4 ай бұрын
​@@henriksundt7148 Hmm. I am sceptical that you can guarantee that the compiler will not do something funny with the ordering of your code when it optimizes it. It would seem that even WITH initialization you cannot guarantee the order of your code... at least according to some c experts on here that refer to the docs etc.
@henriksundt7148
@henriksundt7148 4 ай бұрын
@@noomade There may even not be a stack, as other commenters here also discuss :) But if you assume there is a stack, and check the addresses right after a proper initialisation, I think you can be quite sure about the direction.
@AceofSpades5757
@AceofSpades5757 4 ай бұрын
That was a great exercise. Thanks!
@MeRezMo
@MeRezMo 4 ай бұрын
Haha, fun stuff. This was my one of my interview questions at Microsoft when I tried to intern there. I had limited to no knowledge about OS architecture. It wasn't even the position I was going for. Anyway, I failed it. My initial answer was create 2 variables and compare the address as you've said but it wasn't enough as they can be randomly allocated or as you said optimized. Create 2 functions and creating a variable inside each would be good as they get a full row of memory. Also, it can avoid any complexion like recursion. Still this question hunts me to this date. haha was over 6 years ago. Luck is a big factor in life. Knowledge is what brings that luck closer to us !~ Keep up the good stuff, interesting videos.
@thomashabetsse
@thomashabetsse 4 ай бұрын
It's not just that they may be reordered. It's also that comparing pointers not part of the same array is undefined behavior. You could get back anything. You could get back that x is above y AND y is above x, because with undefined behavior logic does not apply.
@ForsoArk
@ForsoArk 4 ай бұрын
I think i would ve used alloca() which allocates memory in the stack frame and compared the first and last element's address but your recursion based answer seems more platform proof.
@4zv4l38
@4zv4l38 4 ай бұрын
Why alloca instead of an array ?
@dobacetr
@dobacetr 4 ай бұрын
@@4zv4l38I think you need to alloca() twice in sequence and compare their memories. If you use an array and compare its end and beginning, I think it should always show that last-first is positive (because that's the direction an array is oriented in).
@ForsoArk
@ForsoArk 4 ай бұрын
@@dobacetr you are right otherwise indexing wouldn't work. I didn't think of that thank you !
@ForsoArk
@ForsoArk 4 ай бұрын
@@4zv4l38 i might be wrong ( and probably am) but i feel like alloca would always allocate memory in the stack whereas variables and local arrays might be put into register without the volatile keyword also it's just another use case of alloca
@U20E0
@U20E0 4 ай бұрын
The man page i have on alloca can be summarised as "alloca does something, and probably returns valid and unused stack memory in the process" I would only trust alloca when you know everything about the system. The whole purpose of this code is that you don't, so i doubt this is reliable
@rogerparrett3242
@rogerparrett3242 4 ай бұрын
I'd just write a few lines of c, compile with the S option, open gdb, and look at the assembly code. Then, the interviewer would know I knew C, assembly, and how to use a debugger other than printf().😂😂
@steveaguay
@steveaguay Ай бұрын
The second you said recursion it all made sense. Super cool question.
@groogoloog
@groogoloog 4 ай бұрын
I think your second/recursive solution can be incorrect due to tail recursion optimizations (reusing the same stack frame when recursing!) That being said, I’m no expert, so I may be wrong
@natnew32
@natnew32 4 ай бұрын
The issue is, an address to a local variable was passed to the called function- destroying the stack would also destroy the reference.
@moczikgabor
@moczikgabor 3 ай бұрын
The reference itself is not destroyed, just the memory it points to is no longer "reserved". However, pleaee note that the code does not use a reference to a destroyed variable. The second recursive run determines the result, where the first instance of the variable (whose address is in the argument) is still exists. The second run returns the boolean outcome, not the address of its local variable which is going to be destroyed. So theoretically it is not an use-after-free error. Regardless of this, while the question itself is interesting, there is no way to write this program with 100% certainity that it will work in a defined way on all architectures.
@crimsonmentone6527
@crimsonmentone6527 4 ай бұрын
Isn't the logic of the first upordown function wrong? If x adress is greater than y address then the stack is decreasing
@stevel875
@stevel875 4 ай бұрын
On some architectures the result can be non deterministic if called within the context of a larger more complex program ... in at least some implementations of ARM procedure call standards I was familiar with many years ago, stack frames are allocated on the heap as required, so if the inner function call uses the same allocation, the result would be as expected, but if it allocates a new frame on the heap, the result could be either direction.
@weinihao3632
@weinihao3632 4 ай бұрын
int main (int a, char *b[]) { puts (b > *b ? "UP" : "DOWN"); return 0; } or, if system is freestanding: void main (void) { char *a = alloca (1+(int) &main % 5), *b = alloca (1+(int) &main % 5); puts (a < b ? "UP" : "DOWN"); }
@alexandratsankova5825
@alexandratsankova5825 4 ай бұрын
My first idea wad using functions and checking a variable in their scope against eachother, however i didn't think about using recursion and i guess using recursion also eliminates (?) The possibility of the compiler inlining the function and breaking that logic
@clawsie5543
@clawsie5543 4 ай бұрын
On my machine, I get a different result with -O3 because gcc removes recursive call, so...
@redcrafterlppa303
@redcrafterlppa303 4 ай бұрын
You don't really need recursion there. You can make 1 function that returns the address of a volatile local variable as a usize_t (to not confuse people to dereference the invalid pointer/address) And then make the actual upordown function that compares the result of the other function to its volatile local variable. size_t nextStackframeAdrr() { volatile int x = 0; return (usize_t) &x; } bool upordown() { volatile int x = 0; volatile size_t addr = nextStackframeAddr(); volatile size_t addr2 = (size_t) &x; return addr2 < addr; }
@pwii
@pwii 4 ай бұрын
does volatile enforce the order of operations too? from what I know, there is still nothing preventing the compiler from re-ordering things like variable declarations
@redcrafterlppa303
@redcrafterlppa303 4 ай бұрын
@@pwii no. And I'm not sure if this code works korrekt in all cases. But it should work in all cases his version works.
@anon_y_mousse
@anon_y_mousse 4 ай бұрын
@@pwii Indeed. You have to turn off optimizations to get it to work, and if you do, you have no need for the volatile keyword anyway.
@var67
@var67 4 ай бұрын
usize_t is not a predefined or library type. There is size_t which is unsigned, and ssize_t which is signed.
@redcrafterlppa303
@redcrafterlppa303 4 ай бұрын
@@var67 thanks I wasn't sure which way around it was. I usually code in rust where it's size and usize
@greob
@greob 4 ай бұрын
Neat trick! Thanks for sharing!
@BradCypert
@BradCypert 2 ай бұрын
I write almost no C or C++ and this was super fascinating. One of my favorite tech videos in a long, long time. Thank you helping me continually learn every day!
@potatoguy413
@potatoguy413 4 ай бұрын
My first thought was to create a struct with two variables, initialize an instance and compare the pointers of the variables, as structs are required to keep their order.
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
Yes but that will answer about how structs are stored in memory, not how stack allocations are made. There is a difference, and the orders may be different.
@matthewrossee
@matthewrossee 4 ай бұрын
@@GrigoryRechistov How the order can be different?
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
@@matthewrossee Easily. The compiler is allowed to put them in any order or optimize them away. "volatile" will help with the latter but not with the former.
@matthewrossee
@matthewrossee 4 ай бұрын
@@GrigoryRechistov ok that makes sense. What do you mean by optimizing them? From what I understand, the compiler can’t inline the values because then how could you take the address of these variables?
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
@@matthewrossee It cannot inline values of variables but it can values of addresses of variables inline (or make other conclusions like being NULL or non-NULL). Note that the code in the example does not dereference pointers in question, it only operates on addresses themselves.
@MenkoDany
@MenkoDany 4 ай бұрын
Wow, thanks for the video! The first implementation is the one that I had in my head. The only twist I could think of is that the function could be inlined, but that shouldn't affect anything.
3 ай бұрын
As far as I understand the C specification, comparing pointers in this way is undefined behavior, as already explained in other comments. The danger is that such an undefined behavior will probably often work, which makes it hard to discover the problem by testing. One of the most important C skills is to navigate around these undefined behaviors. Therefore, this answer to the interviewer's question arguably shows that, although the interviewee knows things, they more importantly feel too confident writing potentially dangerous code.
@highdefinist9697
@highdefinist9697 4 ай бұрын
Yes, this an interesting one, because (other than knowing what a stack is), the main issue is really the "fighting" against compiler optimizations, hence knowing about "volatile" etc... becomes important.
@JDAndNightrain
@JDAndNightrain 4 ай бұрын
Good video. My only issue is with a boolean function being called upordown. It makes the code read as "Is the stack growing up or down? Yes/No." and does not imply directionality which is the real question we're asking. Something like isGrowingUp() or isGrowingDown() would be better.
@mariosshadow8533
@mariosshadow8533 4 ай бұрын
I tried both local variables, volatile or otherwise, and with recursion, and they both had the same results. Normal compilations reported downward growth, but optimized compilations reported upward growth. Something to keep in mind.
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
This means that the either program's result is unspecified or undefined. Compiler optimization level or compiler choice should not affect outcomes of well-defined C programs. Ergo, this program is not well-defined (see other messages in the comments from many commentators what assumptions are wrong)
@vytah
@vytah 4 ай бұрын
​@@GrigoryRechistovit's the pointer comparison: comparing which of two unrelated pointers is greater is undefined behavior.
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
@@vytah True. Only pointers that refer to insides of the same array or struct/union may be compared, otherwise the C standard (6.5.8p5) explicitly marks the result as undefined behavior.
4 ай бұрын
@@GrigoryRechistovone past the container is also still fine.
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
@ Agree. I always liked this "oh, we have this off-by one problem. All right, let's allow it everywhere"
@andrematheus7527
@andrematheus7527 3 ай бұрын
i didnt understand a single thing you have said through this entire video. loved it 👍
@OnFireByte
@OnFireByte 4 ай бұрын
IIRC popular c compiler like clang and gcc also have tail-call optimization, which can turn this kind of recursion into loop, which I believe can also break like int x, y hack? Disabling inlining might work?
@breadleaf6794
@breadleaf6794 4 ай бұрын
Great video! However I did find something I found interesting when trying to compile both of these solutions... I compiled a program with both of your solutions using gcc 11.4.0 and clang 14.0.0, For your first solution gcc gives Down while clang gives Up... For your second solution both compilers return Down. Edit: I am using pop-os 6.5.6 if anyone else wants to verify this
@khatdubell
@khatdubell 4 ай бұрын
You didn't need to write a recursive function like that. Since all you care about are addresses, and you have no intention of dereferencing them, you can take the address of a variable on the stack in one function, and return that, as a pointer, to the calling function. Then compare that address to the address of a variable in the current function.
@MenaceInc
@MenaceInc 4 ай бұрын
Cursed but I kinda like it
@sanderbos4243
@sanderbos4243 4 ай бұрын
This is the initial version I wrote, thanks for confirming it's valid
@scragar
@scragar 4 ай бұрын
This is how I thought to solve it. Recusion works, but his function does two completely different behaviours based on arguments, which to me screams the need for a distinct separation of the methods, one to return the pointer for a variable on the stack, and another to call it and compare addresses.
@u9vata
@u9vata 4 ай бұрын
Just an excerpt: "The key thing to understand about the 8 bit PIC architecture is that the stack size is fixed. It varies from a depth of 2 for the really low end devices to 31 for the high end 8 bit devices. The most popular parts (such as the 16F877) have a stack size of 8." ^^and they do have C compilers. So there are totally devices with limited hardware stack sizes actually - otherwise its fun little excercise I agree, just sayin' 🙂
@raidtheferry
@raidtheferry 18 күн бұрын
Very cool demonstration
@Gregorius421
@Gregorius421 3 ай бұрын
2:50 the return is inverted. The compiler is also doing the opposite of what you expect it to do: `-O2` and `-O3` optimization reverses the order of the local variables. Thus the result is double wrong, which in boolean logic ends up being right 😄 Also, simply `return &x < &y` will do. `return true; else return false;` is also a telling sign for the interviewer.
@vasishtabhat9280
@vasishtabhat9280 4 ай бұрын
please make videos on volatile, __attribute__ and ## in C
@arta6183
@arta6183 4 ай бұрын
those are completely different things. here is a short explanation of two of them: volatile basically tells the compiler to not optimize whatever you've marked as volatile. I recommend messing around with it in godbolt. ## is some sort of an operator used in the CPP (C Preprocessor) that joins two tokens together, so imagine this: #define JOIN(a, b) a##b then `JOIN(hello,World)` would produce the token `helloWorld`.
@vasishtabhat9280
@vasishtabhat9280 4 ай бұрын
@@arta6183 thanks for info😀 .. i just wanted to know how those features work under the hood...So i suggested separate videos on them
@vitorhugo-yy7cz
@vitorhugo-yy7cz 4 ай бұрын
Great content!
@nicknamemohaji
@nicknamemohaji 4 ай бұрын
How about using inline assembly? Save rsp at local variable in inline frame #1, allocate some new local variables, then check the changed rsp value at inline frame #2. Of course, the local variable should have storage class auto, since static class will make the assembler save variable in other space.
@erikkonstas
@erikkonstas 4 ай бұрын
With this problem, the architecture itself is in question, and every ISA is different...
@sprytnychomik
@sprytnychomik 4 ай бұрын
Plot twist: there is no concept of stack (nor heap) in C standard.
@CallousCoder
@CallousCoder 4 ай бұрын
Indeed it's an operating system and/or hardware implementation. The compiler conforms to that.
@MrEdrftgyuji
@MrEdrftgyuji 4 ай бұрын
There is also no concept of memory addresses in C. The numerical value of a pointer could be anything, as long as it can be dereferenced back to the original variable. Pointer arithmetic and comparison operations are guaranteed to work, though.
@SimonClarkstone
@SimonClarkstone 3 ай бұрын
​@@MrEdrftgyuji Pointer arithmetic and comparisons are not always guaranteed to wwork. As a rule of thumb, if you compare two pointers that aren't identical and don't point within the same array, the result is undefined behaviour.
@TheyCallMeHacked
@TheyCallMeHacked 4 ай бұрын
Wouldn't that break if the compiler decides to do tail call optimisation?
@AnataBakka
@AnataBakka 4 ай бұрын
tail call is impossible in that case, or, rather, even if possible you should assume there is no tail call for that case, see david wragg c tail calls 2014
@natnial1
@natnial1 4 ай бұрын
@@AnataBakkaso basically if the behavior is well defined without TCO but breaks if TCO is applied then it isn’t applied. This of course assumes the program is well defined which it would be as long as long as the pointers are casted before comparing them
@erikkonstas
@erikkonstas 4 ай бұрын
@@natnial1 Yeah, that "well defined" part is where it all breaks; TCO does not "break" anything in regards to well-defined behavior, but here we actually want to tame UB.
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
For those wondering whether it is possible to have a C program run without a stack, I have an example for you. Early x86 firmware boot code has to run for some time *without any RAM all*. That is, no memory for either stack nor for heap. Program's code is read from read-only flash. That is because the memory controller device has not yet been properly programmed. The first thing the firmware might want to do it to make RAM available. But until that moment, one has to deal with lack of memory. One early solution to this was to have a C compiler to produce programs that allocate everything on processor's registers. Needless to say, maximum stack depth is very limited in this situation. But it does not mean you cannot have a standard-compliant compiler for such environment. You might not be able to use recursion at all, but that is because undefined behavior of exhausting implementation-defined recursion limit will kill it early. Which is standard-compliant behavior. For those curious if we still have to use such trick of allocating everything on registers today, then the answer is no. Firmware writers have learned to use CPU's caches as substitute for RAM in early boot. Caches require no configuration to become usable and provide several megabytes of addressable storage, which is much better than half a dozen of 4-byte registers.
@househall
@househall 4 ай бұрын
One of my ideas was to check argument's address with local variable's address, there's huge chance that arguments pushes first before locals are allocated.
@CSalvarani3
@CSalvarani3 4 ай бұрын
The use of recursion is unncessarily complicated. You only ever recurse one layer deep - just use a helper function.
@1vader
@1vader 4 ай бұрын
Yeah, it's also confusing and makes the API worse since you have to pass in NULL to the call for no reason.
@dennismuller1141
@dennismuller1141 4 ай бұрын
I think he did it to prevent a compiler optimization that just moves the function body of the helper function to its only call, therefore creating a similar situation as before, where the compiler can change the order of the variable declarations.
@1vader
@1vader 4 ай бұрын
@@dennismuller1141 Hm, that kinda makes sense. Though I also wouldn't be surprised if the compiler could optimize the recursive call the same way. Tbh in general, I'm not sure there's anything stoping the compiler from optimizing this whole concept into whichever direction it wants, since stack direction doesn't seem to be something that's guaranteed by the spec.
@CSalvarani3
@CSalvarani3 4 ай бұрын
@@dennismuller1141 You make a fair point. Compilers are free to inline functions like you describe. But I don't think the recursive solution is exempt from this. Compilers are pretty smart these days. They are free to "unroll" recursive calls as well as optimize away unreachable branches. So in the end, the same problem arises. The better approach would be to use some compiler annotation, such ass GCC's "noinline" annotation, to prevent such a compiler optimization.
@bbsushi8871
@bbsushi8871 4 ай бұрын
Can't you also compare the address of main with the address of the upordown function?
@martandrmc
@martandrmc 4 ай бұрын
You can't because the functions' code doesn't live in the stack but in the exact opposite side of memory (at least in x86), in the text segment. Only local variables and some other internal values (return address, last frame pointer) get placed in the stack.
@xarkenz
@xarkenz 4 ай бұрын
I don't think that'd work, because the addresses of functions don't point to the stack at all.
@Elesario
@Elesario 4 ай бұрын
The address of the functions won't be in any guaranteed order, but presuming you mean the address of a variable in main vs one in the upordown function, I believe he was trying to avoid the case where the compiler optimises out the call being made and therefore making the comparison not work as intended.
@xarkenz
@xarkenz 4 ай бұрын
@@Elesario Oh, that would make more sense. That's actually a very good idea then.
@JohannY3
@JohannY3 4 ай бұрын
Simply brilliant
@SimonClarkstone
@SimonClarkstone 3 ай бұрын
In some OSes, there is no dedicated stack area but instead stack space is allocated in chunks from the normal memory manager. Therefore, the stack can jump back and forth in memory as it grows.
@robertcorbin2749
@robertcorbin2749 4 ай бұрын
You mention that some architectures have the stack grow +/- differently. Why is this? Why wouldn’t the architectures be consistent with this concept?
@minirop
@minirop 4 ай бұрын
Because they can? architectures are imcompatible with each other, why would they follow the same rules?
@robertcorbin2749
@robertcorbin2749 4 ай бұрын
@@minirop good point. Just curious. I am sure there are some things different architectures can’t do differently. I guess I am wondering why the his isn’t one of them.
@Nezuji
@Nezuji 4 ай бұрын
Cars can drive on either side of the road, but it doesn't matter WHICH side as long as everyone agrees. In a similar way, a computing architecture can grow its stacks "up" or "down", as long as it sticks with one or the other. Stack growth is invisible to 99% of programmers anyway.
@user-pw5do6tu7i
@user-pw5do6tu7i 4 ай бұрын
Upordown is a bad function name IMO. Expected behavior for me would be it nearly always returns true, unless the stack grows horizontally, or non linearly. I think these are better: bool stackGrowsUp bool isGrowingUp
@nikbl4k
@nikbl4k 4 ай бұрын
i like this. very well-explained.
@mochji_
@mochji_ 4 ай бұрын
my first thought was to create a function that takes in an int, returns a pointer to that, then in main create an int and 2 pointers to ints, set the first pointer to the int in main and set the second pointer to the result of theFunctionThatReturnsThePointerOfItsLocal(a) and then compare the 2
@MatteoGodzilla
@MatteoGodzilla 4 ай бұрын
An idea i had in order to solve this was using inner scopes (i.e `int x; { int y; }`) in order to force the inner variable to be after the outer, but i'm not 100% sure that actually happens
@veritas7010
@veritas7010 4 ай бұрын
scopes in c don't allocate new stack frames
@dobacetr
@dobacetr 4 ай бұрын
@@veritas7010I think that's the point. You just need to ensure that variables are allocated in order. Hopefully, x is allocated before entering the scope and y is allocated in the scope such that &y-&x is always the direction of the stack.
@drkspace
@drkspace 4 ай бұрын
You could also just do volatile int x =0; int y=x+1; [rest of the first function] The optimizer cannot change the order since y is dependent on x and x might change between the lines.
@uuuummm9
@uuuummm9 4 ай бұрын
I have a strong feeling that in the situation where i would need to know the direction of the stack there should be much better and reliable way to do it.
@pqnet84
@pqnet84 4 ай бұрын
There is more to that. To account for the inlining and constant propagation you may need to let the number of recursion be dependent on a parameter that the compiler cannot evaluate at compile time, moreover optimization of C allows the compiler to do whatever they want with the pointer comparison, and since the variable are otherwise unused there is no guarantee that an aggressive optimization will not invent whatever result they want for your function. I would probably also use the pointer passed recursively to actually compute something that can't be optimized away (for example, you can keep a running total. I would also if possible rely on some syscall to make sure that the input of the recursive function cannot be guessed at compile time (i.e. using fopen to attempt reading a file) and to use the result of the computation so that it is not discarded and optimized.
@Loki-
@Loki- 4 ай бұрын
It frustrates me to no end that the names are so close together in intuitive meaning that I always forget the difference. A stack of paper is also a heap of paper. Especially annoying that when learning to code stacks, aka first in last out, I'm taught to think of it as stacking napkins up.
@gregoryfenn1462
@gregoryfenn1462 4 ай бұрын
Not really to be fair, a heap of paper would be like if the sheets are thrown at random everywhere on the table, not ina neat pile. A face-down pile of sheets of paper is a stack, not a heap.
@S7hadow
@S7hadow 4 ай бұрын
For me a heap of paper would be a bunch of paper sheets lying on the floor, all in different orientations and no discernible order. A stack on the other hand is all the papers neatly put together and you can clearly go from top to bottom. In that sense, yes, a stack is also a (ordered) heap, but a heap is not necessarily a stack
@iskamag
@iskamag 4 ай бұрын
My immediate idea was to use alloca() Technically not a standard function, but the 3 only compilers that exist support it.
@noomade
@noomade 4 ай бұрын
but some people on here have said that alloca() cannot be guaranteed to give the correct answer.
@alonamaloh
@alonamaloh 4 ай бұрын
Me too! Unlike the C standard, the man page for alloca at least mentions the stack, so we have a better chance of getting the answer we expect. :)
@noomade
@noomade 4 ай бұрын
@@alonamaloh IFF a stack exists right? and even then you can't guarantee that your alloca() does not get optimized away
@alonamaloh
@alonamaloh 4 ай бұрын
​@@noomade Well, a stack must exist if the documentation of alloca says that it "allocates size bytes of space in the stack frame of the caller." If it did anything else, I would say the documentation is wrong. You might be right that an aggressive enough compiler might decide to optimize it away, because by most definitions this is not observable behavior.
@noomade
@noomade 4 ай бұрын
@@alonamaloh I think you should read Grigory Rechistov's posts under this video. He gives compelling arguments as to why alloca is neither a stable nor a guaranteed to be correct solution to this answer (and why no c program can be guaranteed to give the correct answer). He has posts in a few threads with his reasoning.
@stuartwilson4960
@stuartwilson4960 4 ай бұрын
I feel this would be better explained with the assembly listing and a memory view open. The stack could be initialized in different ways, also without the recursion there is pretty much no guarantee that it will grow the stack in any direction. The only correction I would make for intel is to say that the compiled code will use base pointer register offsets (EBP), and not the stack pointer directly, so between call frames there will be some stack preservation wrapper copying to and from EBP/ESP.
@Chris-on5bt
@Chris-on5bt 4 ай бұрын
Not sure if my logic holds, but when you asked the question of a different approach to x,y my first thought was to make a local variable with an array size 2. Then compare element 0 to 1 in terms of address. My assumption is that the compiler would not mix the order of array elements, as part of the definition of an array is it is a serial length of memory. But I don't know specifically how array are allocated on the stack, it very well could be that the first element is stack positionally placed, but then the contained elements are serially placed.
@erikkonstas
@erikkonstas 4 ай бұрын
The problem with this is that ((a + 1) - a) is always 1, it doesn't tell you anything.
@CSalvarani3
@CSalvarani3 4 ай бұрын
The syntax "if ([cond]) return true; else return false;" terrifies me. Just do "return [cond];". Thanks!
@ding.1367
@ding.1367 4 ай бұрын
hai
@LowLevelLearning
@LowLevelLearning 4 ай бұрын
rawr
@ElizabethGreene
@ElizabethGreene 3 ай бұрын
How about declaring x & y, dropping to inline assembly, mov esp, *x push 0 mov esp, *y pop, and then doing the comparison operators on those?
@shoxie95
@shoxie95 4 ай бұрын
Great video !
@peterruszel5389
@peterruszel5389 4 ай бұрын
please name the function is_up if it's going to return true when up for christ sake
@jabadoodle
@jabadoodle 4 ай бұрын
I think you have errors in your code and a number of incorrect/inaccurate statements in your verbalization.
@jabadoodle
@jabadoodle 4 ай бұрын
1: The stack pointer isn't growing "more negative" it's growing "less positive." 2: Your first method returns the wrong value. Note you return First-Variabe>Second-Variable but in the recursion method it's reversed. 3: @2:55 you mention X growing. X isn't growing. Neither is it's pointer. You mean if the difference between &X and &Y is growing. 4: @4:14 you say method one isn't the most efficient. The problem with method-one isn't efficiency, it's that it could be wrong. Big difference.
@toxicitysocks
@toxicitysocks 4 ай бұрын
My first thought was to use a pair of functions instead of a recursive function, but I guess the compiler might optimize that function call away. Recursion seems like a good way around that caveat.
@_nikeee
@_nikeee 4 ай бұрын
I'd try creating a second function and returning the address of a volatile local in it. Then compare it with the address of another volatile local. That doesn't contain recursion but I'm not sure if the call may get inlined, resulting in the same problem as with the first approach.
@__christopher__
@__christopher__ 4 ай бұрын
Of course, comparing to pointers not pointing into the same object or array for anything but (in)equality is undefined behaviour, and the compiler is allowed to do whatever it wants in that case. So in theory the program could output "nice try" instead. So the first assumption is that the optimizer does not take advantage of that undefined behaviour.
@The1wsx10
@The1wsx10 3 ай бұрын
i dont think it "can't get optimised out because we're using recursion" volatile does permit the compiler to make optimisations, it's there for hardware the CPU isn't controlling. the compiler just isn't allowed to assume the value won't change on it's own. other optimisations may happen. there is __attribute__((optnone)) which might suit your use case better. i also think the use of recursion makes the example more complicated than it needs to be. it seems like the condition is the wrong way around (it returns true if the address from the older stack frame is greater. ie, "up" when it's going down.)
@PvblivsAelivs
@PvblivsAelivs 4 ай бұрын
Yes, I can think of an assumption you are making. My go-to solution called an inner function to ensure the variables were in different stack frames. (The compiler _might_ arrange variables in a single stack frame any way it pleases. I didn't actually bother with recursion. I just created a "dummy" function that declared a variable and returned its address. You can still use the _address_ of a memory location that might be reassigned as long as you don't try to use the memory.
@nicholasscott3287
@nicholasscott3287 3 ай бұрын
I would've thought they were asking about the stack trace, so I would've written a function that throws and catches an exception then counts and returns the number of lines in the stack trace, a function that calls that function and returns the number that got returned to it, and then a function that calls both functions and compares the values.
@TheMasonX23
@TheMasonX23 4 ай бұрын
Yay, I'm a non-C programmer who not only knew the first naive approach, but thought about how the compiler could possibly optimize it and ruin things. Didn't quite figure out a solution before I hit play, but I'm still just happy I was on the right track :)
@GrigoryRechistov
@GrigoryRechistov 4 ай бұрын
There is no solution in portable C, because the C standard does not have "stack".
@adamsfusion
@adamsfusion 4 ай бұрын
I'd use the first method mentioned in the video but mark the function as "__attribute__((optimize("O0")))"_ (I hope KZbin doesn't mangle this). Many compilers have the option to _sometimes_ ignore volatile keyword in order to aggressively optimize all loops, allocations, and invocations using something like -O3. The _only_ way to prevent this is to mark the function as no-optimize. Also and importantly, I'd start my answer off with "assuming the architecture I'm on uses a stack." It's totally okay to point out assertions like that and let the interviewer press you on it. The important bit is that you point it out, not that you solve every tiny angle of the problem.
@vytah
@vytah 4 ай бұрын
Another assumption is that comparison of unrelated pointers is reliable, as it's comparison which is the undefined behaviour here.
@Spaggei0hs
@Spaggei0hs 4 ай бұрын
Hi, I wondered if another solution could be to allocate a variable to the stack, and then allocates another to the heap, and compare thoes addresses? Would this be a viable approach, or am I missing something.
@gijsgroenewegen8212
@gijsgroenewegen8212 4 ай бұрын
I was thinking of this solution approach as well. With the assumption the stack and heap are growing towards eachother I think this would be a viable solution.
@user-jn4sw3iw4h
@user-jn4sw3iw4h 4 ай бұрын
I agree it is an interesting question, to see what the interviewee knows. For example, I haven't used C in a long time and (once managed to set aside the "why do we care") I would have guessed the initial solution, and admitted that would be a guess. The then following explanation on, what is considered "common-knowledge" on the abilities or lack thereof of compilers. Is indeed something I would not have been able to contribute to. "Making assumptions on how the compiler treats effects triggered by a single command" (the 2 initializations) was easy to spot as "If the compiler breaks the plan. It would likely do so here". To which my first thought was: - initialize/assign - read address - initialize/assign - read address Might solve it, if we want to be more sure, we probably could: initialize/assign y, with the value of... the address of x. As this information needs to exist, the compiler would have even less room to outsmart the programmer. That C-compilers have a "magical, can't optimize beyond this"-border around recursion. Is not something I knew. (Again, making it a good question.)
@AJMansfield1
@AJMansfield1 4 ай бұрын
I'd have compared the result of an `alloca` call to the address of a local variable -- alloca allocates a dynamic buffer on the stack, so the pointer it returns has to be logically "after" the address of any local variable in a way that isn't guaranteed for two local variables.
@hermand
@hermand 4 ай бұрын
I jumped to recursion as my approach when you first posed the question so I'm putting C on my CV now!
@noomade
@noomade 4 ай бұрын
but the C experts in the comments are saying that the recursive approach offers no guarantee either... and that even those that have gone a step further and used alloca() are also wrong :( Languages: -C programming language-
@jasper265
@jasper265 4 ай бұрын
I like this question a lot. I'm not a C developer, so my first idea was that it would be way out of reach. But then I realized that I could write some pseudo-code. I didn't realize I could just use the addresses of local variables, so my pseudo code used some mysterious method to get the stack pointer. But I was using two separate functions, similar to your recursion (but in my opinion, more appropriate here than recursion, for readability reasons). The amount of reasoning you can do about this even if you don't have the full answer shows how well this question works!
@noomade
@noomade 4 ай бұрын
The best answer on here is the one that explains why this cant be done with any guarantees ( by Grigor)
@john.darksoul
@john.darksoul 4 ай бұрын
Neat! I too fist thought of comparing addresses of variables. However I think using two functions would've been cleaner, I dont like how you are required to call upordown with NULL for it to work.
🔴 Let's build SIGNAL with REACT NATIVE! (Navigation, Expo & Firebase)
3:36:56
How I destroyed a junior developer career
7:46
Boop Beep
Рет қаралды 11 М.
Chips evolution !! 😔😔
00:23
Tibo InShape
Рет қаралды 41 МЛН
This Algorithm is 1,606,240% FASTER
13:31
ThePrimeagen
Рет қаралды 707 М.
everything is open source if you can reverse engineer (try it RIGHT NOW!)
13:56
Low Level Learning
Рет қаралды 1,2 МЛН
the cleanest feature in C that you've probably never heard of
8:13
Low Level Learning
Рет қаралды 124 М.
Projects Every Programmer Should Try
16:58
ThePrimeTime
Рет қаралды 348 М.
everyone codes faster when they stop using their mouse
10:32
Low Level Learning
Рет қаралды 189 М.
When Your Game Is Bad But Your Optimisation Is Genius
8:52
Vercidium
Рет қаралды 1,3 МЛН
new linux exploit is absolutely insane
8:29
Low Level Learning
Рет қаралды 414 М.
Why i think C++ is better than rust
32:48
ThePrimeTime
Рет қаралды 265 М.
The purest coding style, where bugs are near impossible
10:25
Coderized
Рет қаралды 851 М.
Как я сделал домашний кинотеатр
0:41
RICARDO
Рет қаралды 1,5 МЛН
Apple iPhone 15 Pro Max With Smallrig Professional Photography kit #shorts
0:14
Индуктивность и дроссель.
1:00
Hi Dev! – Электроника
Рет қаралды 1,5 МЛН
Трагичная История Девушки 😱🔥
0:58
Смотри Под Чаёк
Рет қаралды 372 М.
Kalem ile Apple Pen Nasıl Yapılır?😱
0:20
Safak Novruz
Рет қаралды 1 МЛН