· 8 years ago · Jun 10, 2018, 09:12 AM
1/*
2
3 Functional programming in C
4 ```````````````````````````
5
6 This is an example of how to use executable memory to get partial function
7 application (i.e. bind1st()) in C. (Well, this actually only compiles as C++
8 since i'm using a varargs typedef, but there's no classes or templates.)
9 To proceed, we need to be comfortable with the cdecl and stdcall calling
10 conventions on x86, and just a little assembly.
11
12 In cdecl, function arguments are pushed onto the x86 stack from right to left,
13 and then control passes to the callee. The callee returns, and then the caller
14 restores the stack to the way it was before continuing on its way. The stdcall
15 convention is similar but the callee is responsible for restoring the stack
16 pointer to clean up after its own arguments.
17
18 It's a very minor difference and most of the time it's inconsequential, you
19 wouldn't even notice it. If you declare a function in C, it uses the cdecl
20 convention unless you specify otherwise. This is normal behaviour for most of
21 the linux world, but every Win32 API is declared stdcall.
22
23 There are some important differences, though: if you have a function with a
24 variable number of arguments - like printf() in the CRT - this would compile
25 to something like
26
27 ; printf( format_str, arg[0] )
28 push dword [ebp+8]
29 push format_str
30 call _printf
31 add esp, 8
32
33 and printf could simply pop an argument off the stack every time it encounters
34 a % character in the format string. It doesn't explicitly count the number of
35 parameters, and therefore has no idea how to clean up after itself, so it
36 must be done as cdecl. There are ways around this - <cstdarg> manages it
37 somehow? - but that's the general idea.
38
39 In the reverse case, if you're using partially applied functions, then the
40 caller sees a function of a single argument, but the target function takes
41 multiple arguments. Unless we can magically unwind the stack some other way,
42 it seems like the callee is the only function that can safely restore the
43 stack, so we must use stdcall for both our target functions and their
44 partially applied forms.
45
46 Partial application involves storing an extra parameter and returning a C
47 function pointer. We really have to end up with a native C function that we
48 can call, one that's portable to other functions and can be thrown around
49 without much special attention. My approach here is to create a stub
50 function on the heap that pushes an extra function argument, and then jmp
51 toward the real target.
52
53 So, to begin, let's figure out executing assembly on the heap:
54
55*/
56
57#include <cstdio>
58#include <Windows.h>
59
60void* xalloc(size_t len) {
61 void* ret = VirtualAlloc(
62 NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
63 );
64
65 if (! ret) {
66 fprintf(stderr, "Fatal: VirtualAlloc() failed\n");
67 exit(1);
68 }
69 return ret;
70}
71
72void xfree(void* ptr) {
73 VirtualFree(ptr, 0, MEM_RELEASE);
74}
75
76/*
77
78 Those are Win32-specific calls, but it should be pretty easy to translate that
79 into linux or any other platform using mmap with the flags parameter set to
80 PROT_READ|PROT_WRITE|PROT_EXECUTE.
81
82*/
83
84typedef void* __stdcall (*heapfn)(...);
85
86heapfn make_heapfn(const char* asmcode, size_t len) {
87 void* ret = xalloc(len);
88 memcpy(ret, asmcode, len);
89 return (heapfn)ret;
90}
91
92/*
93
94 This function takes a string of (i'm assuming i386 here) opcodes and copies
95 them into executable memory on the heap. We have to pass the length separately
96 since C strings are null-terminated, but our assembly could contain a zero.
97
98 That's all we need, so let's try a simple function to make sure everything is
99 OK. In both cdecl and stdcall, the return value from a function is stored in
100 the eax register:
101
102 55 push ebp ; enter function
103 89E5 mov ebp, esp
104 B801000000 mov eax, 0x01 ; return 1
105 C9 leave
106 C3 ret
107
108*/
109
110void test_return() {
111 heapfn fncall = make_heapfn(
112 "\x55\x89\xE5\xB8\x01\x00\x00\x00\xC9\xC3", 10
113 );
114
115 printf("Returns: %d\n", (int)fncall());
116
117 xfree((void*)fncall);
118}
119
120/*
121
122 This prints "Returns: 1".
123
124 We can now implement our stub for partial application, but there's one thing
125 to watch out for - chaining multiple stubs together. The address to return to
126 is pushed onto the stack with `call' and popped with `ret', so we must be
127 careful in order for these to chain along correctly:
128
129 5B pop ebx
130 68xxxxxxxx push dword param1
131 53 push ebx
132 68xxxxxxxx push dword fn_stdcall
133 C3 ret
134
135 Here, we take the return address off the stack, push our extra parameter after
136 the others, and put the correct return address on the stack again (this could
137 be done without clobbering the ebx register). The `jmp' mnemonic is mostly
138 used for relative jumps on x86, so the easiest way to go directly to a pointer
139 is, strangely enough, to push the target onto the stack and then return (or
140 if you're resigned to losing a register, mov eax ptr; jmp eax).
141
142*/
143
144#define B1ST_BSET(s) \
145 (char)((s)&0xFF),(char)(((s)>>8)&0xFF), \
146 (char)(((s)>>16)&0xFF),(char)(((s)>>24)&0xFF)
147
148heapfn bind1st(void* fn_stdcall, void* param1) {
149 char asmcode[13] = {
150 '\x5B', '\x68', B1ST_BSET((int)param1),
151 '\x53', '\x68', B1ST_BSET((int)fn_stdcall), '\xC3'
152 };
153
154 return make_heapfn(asmcode, 13);
155}
156
157/*
158
159 And that's all there is to it. We can define some macros for convenience to
160 stop us adding typecasts everywhere, but if you want type safety this is a
161 ridiculous technique to use (in C++, there's a bind1st for std::function<>
162 objects in <functional> for when a C++11 lambda is inappropriate).
163
164 Here are those macros, and some sample target functions for binding - remember
165 the stdcall declaration! Mistakenly using a stdcall to call a cdecl function
166 only leaves garbage in the stack, but using a cdecl to call a stdcall function
167 removes the arguments twice from the stack and you will crash on return.
168
169*/
170
171#define BIND(type, target, param) \
172 (type __stdcall *(*)(...)) bind1st( (void*)target, (void*) param)
173#define MKBIND(type, varname, target, param) \
174 type __stdcall *(* varname)(...) = BIND(type, target, param)
175
176int __stdcall doubleit(int x) {
177 return x*2;
178}
179
180int __stdcall add(int x, int y) {
181 return x + y;
182}
183
184int __stdcall add3(int x, int y, int z) {
185 return x + y + z;
186}
187
188void test_bind() {
189
190 heapfn myfnc = bind1st((void*)doubleit, (void*)2);
191 printf("Bound: %d\n", (int)myfnc());
192 xfree((void*)myfnc);
193
194 heapfn myadd = bind1st((void*)add, (void*)5);
195 printf("Bound: %d\n", (int)myadd(1));
196 xfree((void*)myadd);
197
198 // Alternatively, using our macros:
199 MKBIND(int, myx, add, 5);
200 printf("Bound: %d\n", myx(1));
201 xfree((void*)myx);
202
203 // Chaining works as expected, although i'm leaking a function here
204 MKBIND(int, recur, BIND(int, add3, 3), 5);
205 printf("3+5+7 = %d\n", recur(7));
206
207}
208
209int main(int, char**) {
210 test_return();
211 test_bind();
212 return 0;
213}
214
215/*
216
217 That's all! Partial function application in C. For C++. For 32-bit Windows.
218 Built with nasm, ndisasm and MinGW GCC 4.6.2. Please don't ever use this.
219
220 - Mason <mappu zero four at gmail.com>, July 2012
221
222*/