拆卸简单的C功能

时间:2016-04-08 14:05:17

标签: c assembly x86

我试图了解简单C函数的底层程序集。

program1.c

void function() {
  char buffer[1];
}

=>

push  %ebp
mov   %esp, %ebp
sub   $0x10, %esp
leave
ret

不确定它是如何到达0x10的?不是字符1字节,即8位,所以它应该是0x08?

program2.c

void function() {
  char buffer[4];
}

=>

push  %ebp
mov   %esp, %ebp
sub   $0x18, %esp
mov   ...
mov   ...
[a bunch of random instructions]

不确定它是如何到达0x18的?另外,为什么在SUB指令之后有那么多附加指令?我所做的就是将数组的长度从1改为4。

4 个答案:

答案 0 :(得分:6)

gcc uses -mpreferred-stack-boundary=4 by default适用于x86 32和64位ABI,因此它会保持{ "$type": "Tfl.Api.Presentation.Entities.PlacesResponse, Tfl.Api.Presentation.Entities", "centrePoint": [ 51.555, 0.059 ], "places": [{ "$type": "Tfl.Api.Presentation.Entities.StopPoint, Tfl.Api.Presentation.Entities", "naptanId": "490009219W", "indicator": "Stop B", "stopLetter": "B", "modes": [ "bus" ], "icsCode": "1009219", "stopType": "NaptanPublicBusCoachTram", "stationNaptan": "490G00009219", "lines": [{ "$type": "Tfl.Api.Presentation.Entities.Identifier, Tfl.Api.Presentation.Entities", "id": "25", "name": "25", "uri": "/Line/25", "type": "Line" }, { "$type": "Tfl.Api.Presentation.Entities.Identifier, Tfl.Api.Presentation.Entities", "id": "86", "name": "86", "uri": "/Line/86", "type": "Line" }, { "$type": "Tfl.Api.Presentation.Entities.Identifier, Tfl.Api.Presentation.Entities", "id": "w19", "name": "W19", "uri": "/Line/w19", "type": "Line" }], "lineGroup": [{ "$type": "Tfl.Api.Presentation.Entities.LineGroup, Tfl.Api.Presentation.Entities", "naptanIdReference": "490009219W", "stationAtcoCode": "490G00009219", "lineIdentifier": [ "25", "86", "w19" ] }], "lineModeGroups": [{ "$type": "Tfl.Api.Presentation.Entities.LineModeGroup, Tfl.Api.Presentation.Entities", "modeName": "bus", "lineIdentifier": [ "25", "86", "w19" ] }], "status": true, "id": "490009219W", "commonName": "Little Ilford Lane", "distance": 64.10041498232529, "placeType": "StopPoint", "additionalProperties": [{ "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities", "category": "Direction", "key": "CompassPoint", "sourceSystemKey": "Naptan490", "value": "W" }, { "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities", "category": "Direction", "key": "Towards", "sourceSystemKey": "CountDown", "value": "East Ham or Manor Park" }], "lat": 51.554475, "lon": 0.059381 }] } 16B对齐。

我能够使用gcc 4.8.2 -O0 -m32 on the Godbolt Compiler Explorer

重现您的输出
%esp

您必须使用默认启用的gcc with -fstack-protector版本。较新的gcc通常不配置为执行此操作,因此您不会获得相同的sentinel值并检查写入堆栈。 (在那个Godbolt链接中尝试更新的gcc)

void f1() { char buffer[1]; }
    pushl   %ebp
    movl    %esp, %ebp      # make a stack frame (`enter` is super slow, so gcc doesn't use it)
    subl    $16, %esp
    leave                   # `leave` is not terrible compared to mov/pop
    ret

显然gcc认为void f4() { char buffer[4]; } pushl %ebp # movl %esp, %ebp # make a stack frame subl $24, %esp # IDK why it reserves 24, rather than 16 or 32B, but prob. has something to do with aligning the stack for the possible call to __stack_chk_fail movl %gs:20, %eax # load a value from thread-local storage movl %eax, -12(%ebp) # store it on the stack xorl %eax, %eax # tmp59 movl -12(%ebp), %eax # D.1377, tmp60 xorl %gs:20, %eax # check that the sentinel value matches what we stored je .L3 #, call __stack_chk_fail # .L3: leave ret 是“易受攻击的对象”,而不是char buffer[4]。没有char buffer[1],即使在-fstack-protector,asm也几乎没有差异。

答案 1 :(得分:2)

  

不是字符1字节,即8位,所以它应该是0x08?

这个值不是位,而是字节。

  

不确定它是如何到达0x10的?

这一行:

push  %ebp
mov   %esp, %ebp
sub   $0x10, %esp

在堆栈上分配空间,保留16个字节的内存用于执行此功能。

存储以下信息需要所有这些字节:

  • 将在ret指令
  • 中跳转到的指令的4字节存储器地址
  • 函数的局部变量
  • Data structure alignment
  • 我现在不记得的其他东西:)

在您的示例中,分配了16个字节。其中4个用于将被调用的下一条指令的地址,因此我们剩下12个字节。 1个字节用于大小为1的char数组,可能由编译器优化为单个char。最后11个字节可能存储了一些我记不住的东西,并且编译器添加了填充。

  

不确定它是如何到达0x18的?

第二个示例中的每个附加字节增加了2个字节的堆栈大小,char的1个字节,以及1个用于内存对齐的字节。

  

另外,为什么SUB指令后会有这么多附加指令?

请按照说明更新问题。

答案 2 :(得分:2)

此代码只是设置堆栈帧。这用作局部变量的临时空间,并且会有某种对齐要求。

您还没有提到您的平台,所以我无法确切地告诉您系统的要求是什么,但显然这两个值至少是8字节对齐的(所以局部变量的大小是四舍五入所以%esp仍然是8的倍数。

搜索“c function prolog epilog”“c function call stack”以查找此区域的更多资源。

编辑 - Peter Cordes的回答解释了差异和神秘的额外说明

为了完整起见,尽管法比奥已经回答了这一部分:

  

不确定它是如何到达0x10的?不是字符1字节,即8位,所以它应该是0x08?

在x86上,%esp是堆栈指针,指针存储地址,这些是字节地址。子字节寻址很少使用(参见Peter的评论)。如果要检查字节内的各个位,通常会对值使用按位(&|~^)运算,但不会更改地址。

(你可以同样认为,子缓存行寻址是一个方便的小说,但我们正在迅速脱离主题)。

答案 3 :(得分:-1)

每当你分配内存时,你的操作系统几乎从来没有真正给你那个数量,除非你使用像pvalloc这样的函数,它给你一个页面对齐的字节数(通常是4K)。相反,您的操作系统假定您将来可能需要更多,所以继续并为您提供更多。

要禁用此行为,请使用不执行缓冲的较低级别系统调用,例如sbrk()。这些讲义是一个很好的资源: http://web.eecs.utk.edu/~plank/plank/classes/cs360/360/notes/Malloc1/lecture.html