📜 ⬆️ ⬇️

memset - side of darkness



After reading the article The most dangerous function in the world of C / C ++, I found it useful to go deeper into the evil in the dark cellar of memset , and write a supplement in order to broaden the essence of the problem.

In C, memset () is commonly used, concealing many pitfalls. Excerpt from C ++ Reference:
void * memset (void * ptr, int value, size_t num);
Fill block of memory
It can be interpreted as interpreted by the text.
Parameters
ptr - pointer to memory.
value - Value to be set. The value of the fills is what the block of memory uses.
num - Number to by value. size_t is an unsigned integral type.
Return value
ptr is returned.

As already noted several times, there are many rakes, which are attacked even by experienced developers. From the Andrey2008 article described in the article , a brief summary of common errors :

№1. Trying to calculate the size of an array or structure, do not use sizeof () for pointers to an array / structure, it will return you a pointer size of 4 or 8 bytes, instead of the size of the array / structure.
')
№2. The third argument, memset (), takes as input the number of bytes, not the number of elements, not taking into account the data type. I will add more, for example, the type int can occupy both 4 and 8 bytes, depending on the architecture. In this case, use sizeof (int).

Number 3. Do not confuse the arguments. The correct sequence is a pointer, a value, a length in bytes.

№4. Do not use memset when working with class objects.

But this is only the tip of the iceberg.

Alternative to memset


memset is a low-level function that binds the developer to take into account all the features of the computer architecture and its use must be justified. Let's first consider the alternative = {0} , instead of memset , they say it allows you to initialize an array or string at the compilation stage, which should improve program performance, unlike memset (also ZeroMemory), which initialize data at runtime. I decided to check it out.

void doInitialize() { char p0[25] = {0} ; //   25   0 char p1[25] = "" ; //   25   0 wchar_t p2[25] = {0} ; //  25   0 wchar_t p3[25] = L"" ; //   25   0 short p4[62] = {0} //  62   0 int p5[37] = {-1} ; //      -1 unsigned int p6[10] = {89} ; //     89 } 

C99 [$ 6.7.8 / 21]
If you’re trying to find out what to do, then be initialized implicitly storage duration.

At the same time, such initialization removes problems No. 1, No. 2, No. 3 with a confusion of parameters and buffer sizes. That is, we will not confuse the second and third arguments in some places, it is not necessary to transfer the size. Let's see how compilers transform such code. I can not check all the compilers right away, the gcc included in android-ndk-r10c, as well as gcc in ubuntu 14.04, were on hand.

gcc -v
1) gcc version 4.9 20140827 (prerelease) (GCC)
2) gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)

Let's see how the compiler behaves on such a piece of code:

 void empty_string(){ int i; char p1[25] = {0}; printf("\np1: "); for (i = 0; i < 25; i++) printf("%x,",p1[i]); } 

So, without optimization (-O0), array initialization is compiled into such an assembler code (we scan the binaries with the help of objdump):

gcc -O0, ELF 32-bit, ARM, EABI5
  83d8: e3a03000 mov r3, #0 83dc: e50b3024 str r3, [fp, #-36] ; 0x24 83e0: e24b3020 sub r3, fp, #32 83e4: e3a02000 mov r2, #0 83e8: e5832000 str r2, [r3] 83ec: e2833004 add r3, r3, #4 83f0: e3a02000 mov r2, #0 83f4: e5832000 str r2, [r3] 83f8: e2833004 add r3, r3, #4 83fc: e3a02000 mov r2, #0 8400: e5832000 str r2, [r3] 8404: e2833004 add r3, r3, #4 8408: e3a02000 mov r2, #0 840c: e5832000 str r2, [r3] 8410: e2833004 add r3, r3, #4 8414: e3a02000 mov r2, #0 8418: e5832000 str r2, [r3] 841c: e2833004 add r3, r3, #4 8420: e3a02000 mov r2, #0 8424: e5c32000 strb r2, [r3] 8428: e2833001 add r3, r3, #1 


gcc -O0, ELF 64-bit, x86-64
  400700: 48 c7 45 d0 00 00 00 00 movq $0x0,-0x30(%rbp) 400708: 48 c7 45 d8 00 00 00 00 movq $0x0,-0x28(%rbp) 400710: 48 c7 45 e0 00 00 00 00 movq $0x0,-0x20(%rbp) 400718: c6 45 e8 00 movb $0x0,-0x18(%rbp) 


As expected, without optimization, we get run-time code that will eat O (n) processor time (where n is the buffer length). What the compiler will do with optimization (-O3) can be seen below.

gcc -O3, 32-bit, ARM

 000083ac <empty_string>: 83ac: e59f002c ldr r0, [pc, #44] ; 83e0 <empty_string+0x34> 83b0: e92d4038 push {r3, r4, r5, lr} 83b4: e08f0000 add r0, pc, r0 83b8: ebffffb2 bl 8288 <printf@plt> 83bc: e59f5020 ldr r5, [pc, #32] ; 83e4 <empty_string+0x38> 83c0: e3a04019 mov r4, #25 83c4: e08f5005 add r5, pc, r5 83c8: e1a00005 mov r0, r5 83cc: e3a01000 mov r1, #0 83d0: ebffffac bl 8288 <printf@plt> 83d4: e2544001 subs r4, r4, #1 83d8: 1afffffa bne 83c8 <empty_string+0x1c> 83dc: e8bd8038 pop {r3, r4, r5, pc} 
gcc -O3, 64-bit, x86-64
 00000000004006d0 <empty_string>: 4006d0: 53 push %rbx 4006d1: be a4 08 40 00 mov $0x4008a4,%esi 4006d6: bf 01 00 00 00 mov $0x1,%edi 4006db: 31 c0 xor %eax,%eax 4006dd: bb 32 00 00 00 mov $0x32,%ebx 4006e2: e8 d9 fd ff ff callq 4004c0 <__printf_chk@plt> 4006e7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 4006ee: 00 00 4006f0: 31 d2 xor %edx,%edx 4006f2: 31 c0 xor %eax,%eax 4006f4: be aa 08 40 00 mov $0x4008aa,%esi 4006f9: bf 01 00 00 00 mov $0x1,%edi 4006fe: e8 bd fd ff ff callq 4004c0 <__printf_chk@plt> 400703: 83 eb 01 sub $0x1,%ebx 400706: 75 e8 jne 4006f0 <empty_string+0x20> 400708: 5b pop %rbx 400709: c3 retq 


We see that the piece of code with zeroing in the run-time just disappeared, we got the promised performance O (1), let's see where does printf take its values? We are interested in this piece:

 83bc: ldr r5, [pc, #32] 83c0: mov r4, #25 ;//  r4    for,     83c4: add r5, pc, r5 ;//  r5   "%x,"  ,      002c7825 83c8: mov r0, r5 ;// r5    r0    ,    printf() 83cc: mov r1, #0 ;//   0 (  p1[i])    printf() 83d0: bl 8288 <printf@plt> 83d4: subs r4, r4, #1 ;//      83d8: bne 83c8 <empty_string+0x1c> ;//     0,      83c8 

That is, the compiler simply threw out the array, and instead of its values ​​it uses 0, as a constant pledged at the compile stage. Good, but what happens if we use memset ? Let's look at a few pieces of objdump, for example, under ARM:

Without optimization -O0 :

  83d8: e24b3024 sub r3, fp, #36 ; 0x24 83dc: e1a00003 mov r0, r3 83e0: e3a01000 mov r1, #0 83e4: e3a02019 mov r2, #25 83e8: ebffffa3 bl 827c <memset@plt> 

Optimized with -O3 :

  83c0: e58d3004 str r3, [sp, #4] 83c4: e58d3008 str r3, [sp, #8] 83c8: e58d300c str r3, [sp, #12] 83cc: e58d3010 str r3, [sp, #16] 83d0: e58d3014 str r3, [sp, #20] 83d4: e58d3018 str r3, [sp, #24] 83d8: e5cd301c strb r3, [sp, #28] 

x86-64
Without optimization -O0:
  400816: ba 19 00 00 00 mov $0x19,%edx 40081b: be 00 00 00 00 mov $0x0,%esi 400820: 48 89 c7 mov %rax,%rdi 400823: e8 a8 fc ff ff callq 4004d0 <memset@plt> 

Optimized with -O3:
  4007f4: 48 c7 04 24 00 00 00 00 movq $0x0,(%rsp) 4007fc: 48 c7 44 24 08 00 00 00 movq $0x0,0x8(%rsp) 400805: 48 c7 44 24 10 00 00 00 movq $0x0,0x10(%rsp) 40080e: c6 44 24 18 00 movb $0x0,0x18(%rsp) 


That is, the optimization simply removes the memset call, inserting it inline. In such cases, memset will always work for O (n) time, but initialization with = {0} when optimizing works for a constant, in our case, without at all taking away processor cycles, brazenly throwing out the fact of the existence of an array and replacing all its elements zeros. But let's see if this is always the case and what happens if we write a non-zero value after initialization? The test function will look like this:

 void empty_string(){ int i; char p1[25] = {0}; p1[0] = 65; printf("\np1: "); for (i = 0; i < 25; i++) printf("%x,",p1[i]); } 

After compilation we get the already familiar block of code:

  8404: e3a02041 mov r2, #65 ; 0x41 8408: e08f0000 add r0, pc, r0 840c: e58d3004 str r3, [sp, #4] 8410: e58d3008 str r3, [sp, #8] 8414: e58d300c str r3, [sp, #12] 8418: e58d3010 str r3, [sp, #16] 841c: e58d3014 str r3, [sp, #20] 8420: e58d3018 str r3, [sp, #24] 8424: e5cd301c strb r3, [sp, #28] 8428: e5cd2004 strb r2, [sp, #4] 

x86-64
  4006f8: 48 c7 04 24 00 00 00 movq $0x0,(%rsp) 4006ff: 00 400700: 48 c7 44 24 08 00 00 movq $0x0,0x8(%rsp) 400707: 00 00 400709: 48 c7 44 24 10 00 00 movq $0x0,0x10(%rsp) 400710: 00 00 400712: c6 44 24 18 00 movb $0x0,0x18(%rsp) 400717: c6 04 24 41 movb $0x41,(%rsp) 


And it looks as if the compiler has inserted into us an optimized memset version. And let's see what happens if the size of the array grows significantly? Say, not 25 bytes, but 25 kilobytes!

  83fc: e24ddc61 sub sp, sp, #24832 ; 0x6100 8400: e24dd0a8 sub sp, sp, #168 ; 0xa8 8404: e3a01000 mov r1, #0 8408: e59f2054 ldr r2, [pc, #84] ; 8464 <empty_string+0x6c> 840c: e1a0000d mov r0, sp 8410: ebffff99 bl 827c <memset@plt> 

x86-64
  400720: 55 push %rbp 400721: ba a8 61 00 00 mov $0x61a8,%edx 400726: 31 f6 xor %esi,%esi 400728: 53 push %rbx 400729: 48 81 ec b8 61 00 00 sub $0x61b8,%rsp 400730: 48 89 e7 mov %rsp,%rdi 400733: 48 8d ac 24 a8 61 00 lea 0x61a8(%rsp),%rbp 40073a: 00 40073b: 48 89 e3 mov %rsp,%rbx 40073e: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 400745: 00 00 400747: 48 89 84 24 a8 61 00 mov %rax,0x61a8(%rsp) 40074e: 00 40074f: 31 c0 xor %eax,%eax 400751: e8 8a fd ff ff callq 4004e0 <memset@plt> 400756: be 54 09 40 00 mov $0x400954,%esi 40075b: bf 01 00 00 00 mov $0x1,%edi 400760: 31 c0 xor %eax,%eax 400762: c6 04 24 41 movb $0x41,(%rsp) 400766: e8 a5 fd ff ff callq 400510 <__printf_chk@plt> 40076b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 400770: 0f be 13 movsbl (%rbx),%edx 400773: 31 c0 xor %eax,%eax 400775: be 5a 09 40 00 mov $0x40095a,%esi 40077a: bf 01 00 00 00 mov $0x1,%edi 40077f: 48 83 c3 01 add $0x1,%rbx 400783: e8 88 fd ff ff callq 400510 <__printf_chk@plt> 400788: 48 39 eb cmp %rbp,%rbx 40078b: 75 e3 jne 400770 <empty1_string+0x50> 40078d: 48 8b 84 24 a8 61 00 mov 0x61a8(%rsp),%rax 400794: 00 400795: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 40079c: 00 00 40079e: 75 0a jne 4007aa <empty1_string+0x8a> 4007a0: 48 81 c4 b8 61 00 00 add $0x61b8,%rsp 4007a7: 5b pop %rbx 4007a8: 5d pop %rbp 4007a9: c3 retq 


Wow!

Line = {0} goes to the side of darkness, memset rejoices!

However, let's not forget, yet we decided to confuse the problem with the parameters; now it’s impossible to mix up the arguments.

String initialization


Also, it would not be superfluous to consider the option of initializing the array = "" . In the C language, null-terminated strings are used, that is, the first character with a byte value of 0x00 indicates the end of the string. Therefore, to initialize a string, it makes no sense to reset all elements; you just need to zero the first one. Here are some ways to initialize an empty string:

 void doInitializeCString() { char p0[25] = {0} ; //     0 char p1[25] = "" ; //     0 char p2[25] ; p2[0] = 0 ; //     0 char p3[25] ; memset(p3, 0, sizeof(p3)) ; //  25   0 char p4[25] ; strcpy(p4, "") ; //     0 char *p5 = (char *) calloc(25, sizeof(char)) ; //     0 } 

The surest way how initialization will work with = "" will parse the objdump again. Without optimization, we will not see anything special, everything is similar there = {0} , we will immediately consider with the -O3 option. So compile under ARM:
such a function
 void empty_string(){ int i; char p1[25] = ""; printf("\np1: "); for (i = 0; i < 25; i++) printf("%x,",p1[i]); } 


And, suddenly, we get zeroing of all elements of the array.

  83c0: e58d3004 str r3, [sp, #4] 83c4: e58d3008 str r3, [sp, #8] 83c8: e58d300c str r3, [sp, #12] 83cc: e58d3010 str r3, [sp, #16] 83d0: e58d3014 str r3, [sp, #20] 83d4: e58d3018 str r3, [sp, #24] 83d8: e5cd301c strb r3, [sp, #28] 

x86-64
  400768: 48 c7 04 24 00 00 00 00 movq $0x0,(%rsp) 400770: 48 c7 44 24 08 00 00 00 movq $0x0,0x8(%rsp) 400779: 48 c7 44 24 10 00 00 00 movq $0x0,0x10(%rsp) 400782: c6 44 24 18 00 movb $0x0,0x18(%rsp) 


Oh well! Why in the null-terminated string reset all unused characters? It is enough to zero one single byte. Hmm, and if there will be 25 thousand bytes, what will it do? Here's what:

  8474: e24ddc61 sub sp, sp, #24832 ; 0x6100 8478: e24dd0a8 sub sp, sp, #168 ; 0xa8 847c: e3a0c000 mov ip, #0 8480: e28d3f6a add r3, sp, #424 ; 0x1a8 8484: e1a0100c mov r1, ip 8488: e59f204c ldr r2, [pc, #76] ; 84dc <empty_string+0x6c> 848c: e28d0004 add r0, sp, #4 8490: e503c1a8 str ip, [r3, #-424] ; 0x1a8 8494: ebffff78 bl 827c <memset@plt> 

x86-64
 00000000004007b0 <empty_string>: 4007b0: 55 push %rbp 4007b1: ba a0 61 00 00 mov $0x61a0,%edx 4007b6: 31 f6 xor %esi,%esi 4007b8: 53 push %rbx 4007b9: 48 81 ec b8 61 00 00 sub $0x61b8,%rsp 4007c0: 48 8d 7c 24 08 lea 0x8(%rsp),%rdi 4007c5: 48 8d ac 24 a8 61 00 lea 0x61a8(%rsp),%rbp 4007cc: 00 4007cd: 48 c7 04 24 00 00 00 movq $0x0,(%rsp) 4007d4: 00 4007d5: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 4007dc: 00 00 4007de: 48 89 84 24 a8 61 00 mov %rax,0x61a8(%rsp) 4007e5: 00 4007e6: 31 c0 xor %eax,%eax 4007e8: 48 89 e3 mov %rsp,%rbx 4007eb: e8 f0 fc ff ff callq 4004e0 <memset@plt> 4007f0: be 54 09 40 00 mov $0x400954,%esi 4007f5: bf 01 00 00 00 mov $0x1,%edi 4007fa: 31 c0 xor %eax,%eax 4007fc: e8 0f fd ff ff callq 400510 <__printf_chk@plt> 400801: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 400808: 0f be 13 movsbl (%rbx),%edx 40080b: 31 c0 xor %eax,%eax 40080d: be 5a 09 40 00 mov $0x40095a,%esi 400812: bf 01 00 00 00 mov $0x1,%edi 400817: 48 83 c3 01 add $0x1,%rbx 40081b: e8 f0 fc ff ff callq 400510 <__printf_chk@plt> 400820: 48 39 eb cmp %rbp,%rbx 400823: 75 e3 jne 400808 <empty_string+0x58> 400825: 48 8b 84 24 a8 61 00 mov 0x61a8(%rsp),%rax 40082c: 00 40082d: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 400834: 00 00 400836: 75 0a jne 400842 <empty_string+0x92> 400838: 48 81 c4 b8 61 00 00 add $0x61b8,%rsp 40083f: 5b pop %rbx 400840: 5d pop %rbp 400841: c3 retq 


Looks like dark memset is haunting us. If you still want to fight against darkness, then it is worth mentioning what other traps are waiting for you.



memset may initialize numbers with incorrect values.


If you want to fill an array of integers with nonzero values, read byte-filled data.

 void doInitializeToMistakenValues() { char pChar[25] ; unsigned char pUChar[25] ; short pShort[25] ; unsigned short pUShort[25] ; int pInt[25] ; unsigned int pUInt[25] ; //  2-  4-      memset(pChar, 1, sizeof(pChar)) ; // 1 memset(pUChar, 1, sizeof(pUChar)) ; // 1 memset(pShort, 1, sizeof(pShort)) ; // 257 memset(pUShort, 1, sizeof(pUShort)) ; // 257 memset(pInt, 1, sizeof(pInt)) ; // 16843009 memset(pUInt, 1, sizeof(pUInt)) ; // 16843009 //  unsigned    0xFF memset(pChar, -1, sizeof(pChar)) ; // -1 memset(pUChar, -1, sizeof(pUChar)) ; // 255 memset(pShort, -1, sizeof(pShort)) ; // -1 memset(pUShort, -1, sizeof(pUShort)) ; // 65535 memset(pInt, -1, sizeof(pInt)) ; // -1 memset(pUInt, -1, sizeof(pUInt)) ; // 4294967295 } 

Consider how it turns out. Here we have, let's say, an array of int, passing the unit to the second parameter, what happens?

Here's what:

0x01010101 - in hexadecimal notation, each byte will be filled with one, and the correct value
0x00000001 will not be available with the memset function. But in fact, this is not a bug, this is a feature.

Here only ignorance of these features leads to unpredictable errors.

memset can set an invalid value


If the double elements are set to bytes -1, we get the value Not-A-Number (NaN), and after subsequent calculations, each operation with the value NaN will return NaN, thus disrupting the whole chain of calculations.

In the same way, setting -1 to the bool type is incorrect and it formally will not be either true or false. Although in most cases it will behave like true. In most cases…

And the last, memset is intended only for work with simple data structures . Never use memset with managed data structures, this function is intended only for low-level operations.


The article used materials memset is evil .

Also read about the printf vulnerabilities .

Source: https://habr.com/ru/post/272269/


All Articles