Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

If I do not pass enough parameters when calling a function in a DLL, what will happen?

Tags:

c++

c

dll

In a dll project, the function is like this:

extern "C" __declspec(dllexport) void foo(const wchar_t* a, const wchar_t* b, const wchar_t* c)

In a different project, I will use foo function, but I declare foo function in header file with

extern "C" __declspec(dllimport) void foo(const wchar_t* a, const wchar_t* b)

and I call it with only two parameters.

The result is success, I think it about __cdecl call, but I would like to know how and why this works.

like image 751
Frank Avatar asked Mar 31 '17 07:03

Frank


People also ask

What happens when you create a function without any parameters?

(1) The variable side is not given to the parameter, but directly accessed by the function which is possible in JavaScript. (2) The function does not return any value but prints the output to the browser console.

Is it necessary to pass all the arguments to call a function?

Arguments are passed by value; that is, when a function is called, the parameter receives a copy of the argument's value, not its address. This rule applies to all scalar values, structures, and unions passed as arguments. Modifying a parameter does not modify the corresponding argument passed by the function call.

How many parameters can be passed to function?

Theoretically you can set these max stack size to 8192 bits. Each variable takes up 32 bits then you could pass 256 parameters. 8192/32 = 256. There is no maximum limit to pass parameters or arguments to a user defined function.

What is passing arguments to function?

Passing arguments to function is a very important aspect of C++ programming. Arguments refer to values that can be passed to a function. Furthermore, this passing of arguments takes place for the purpose of being used as input information.


1 Answers

32-bit

Default calling convention is __cdecl, which means the caller pushes parameters onto the stack right-to-left then cleans up the stack after the call returns.

So in your case, the caller:

  1. Pushes b
  2. Pushes a
  3. Pushes the return address
  4. Calls the function.

At this point the stack looks like this (assume 4 byte pointers for example, and remember the stack pointer travels backwards when you push things):

+-----+ <--- this is where esp is after pushing stuff
| ret | [esp]
+-----+
|  a  | [esp+4]
+-----+
|  b  | [esp+8]
+-----+ <--- this is where esp was before we started
| ??? | [esp+12 and beyond]
+-----+

Ok, great. Now the problem happens on the callee side. The callee is expecting parameters to be at certain locations on the stack, so:

  • a is assumed to be at [esp+4]
  • b is assumed to be at [esp+8]
  • c is assumed to be at [esp+12]

And this is where the issue is: We have no idea what's at [esp+12]. So the callee will see the correct values of a and b, but will interpret whatever unknown garbage happens to be at [esp+12] as c.

At that point it's pretty much undefined, and depends on what your function actually does with c.

After all this is over and the callee returns, assuming your program didn't crash, the caller will restore esp and the stack pointer will be back where it should be. So from the caller's POV everything is probably fine and the stack pointer ends up back where it's supposed to be, but the callee sees junk for c.


64-bit

The mechanics on 64-bit machines is different but the end result is roughly the same effect. Microsoft uses the following calling convention on 64-bit machines regardless of __cdecl or whatever (any convention you specify is ignored and all are treated identically):

  • First four integer or pointer arguments placed in registers rcx, rdx, r8, and r9, in that order, left-to-right.
  • First four floating-point arguments placed in registers xmm0, xmm1, xmm2, and xmm3, in that order, left-to-right.
  • Anything remaining is pushed to the stack, right-to-left.
  • The caller is responsible for restoring esp as well as restoring the values of all volatile registers after the call.

So in your case, the caller:

  1. Puts a in rcx.
  2. Puts b in rdx.
  3. Allocates an extra 32 bytes of "shadow space" on the stack (see that MS article).
  4. Pushes the return address.
  5. Calls the function.

But the callee is expecting:

  • a assumed to be in rcx (check!)
  • b assumed to be in rdx (check!)
  • c assumed to be in r8 (problem)

And so, as with the 32-bit case, the callee interprets whatever happened to be in r8 as c, and potential hijinks ensue, with the end effect depending on what the callee does with c. When it returns, assuming the program did not crash, the caller restores all volatile registers (rcx and rdx, and also generally includes r8 and friends) and restores esp.

like image 189
Jason C Avatar answered Oct 27 '22 00:10

Jason C