Skip to content

[Zend]: Fix unnecessary alignment in ZEND_CALL_FRAME_SLOT macro #10988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 4, 2023
Merged

[Zend]: Fix unnecessary alignment in ZEND_CALL_FRAME_SLOT macro #10988

merged 2 commits into from
Apr 4, 2023

Conversation

stkeke
Copy link
Contributor

@stkeke stkeke commented Apr 1, 2023

I spent almost one hour trying to understand the purpose of alignment while calculating ZEND_CALL_FRAME_SLOT and finally found that it might not be necessary. Here is my arguments:

  1. ZEND_MM_ALIGNED_SIZE(sizeof(zval)) cleans the last 3 bits of
    sizeof(zval) and actually makes it smaller.
    If sizeof(zval) < 8, there will be a divied by zero error.

  2. Aligning of the result of sizeof() opration is logically hard to
    understand and does not make sense for code beginners like me.
    (address can be aligned, but size should not.)

Why we have not got trouble so far?
Because sizeof(zend_execute_data) is 80 and sizeof(zval) is 16 in current code. Therefore, Alignment operations take no effect. If size changed in future, we might get logic trouble.

This patch cleans this macro and makes it logically correct and code beginner friendly.

@stkeke
Copy link
Contributor Author

stkeke commented Apr 1, 2023

@iluuu1994 I believe my reasoning is logically correct, but not 100% confident.
If I am wrong, let's simply drop and close this PR. Thanks.

@nielsdos
Copy link
Member

nielsdos commented Apr 1, 2023

ZEND_MM_ALIGNED_SIZE(sizeof(zval)) cleans the last 3 bits of
sizeof(zval) and actually makes it smaller.
If sizeof(zval) < 8, there will be a divied by zero error.

I just checked this, because if the ZEND_MM_ALIGNED_SIZE is wrong, then this will cause problems for a lot of code in PHP. But the macro is correct. It does not make it smaller, because before cleaning the last 3 bits it adds the alignment-1. So it actually rounds up to the nearest multiple of the alignment. Check this godbolt example: https://godbolt.org/z/EYfqasEjs Even if the size is 1 it correctly rounds up to 8.

So there isn't a bug.

Whether the alignment is necessary for the call frame, I have not checked that.

@stkeke
Copy link
Contributor Author

stkeke commented Apr 1, 2023

@nielsdos You're right. Alignment is actually make address bigger, rather than smaller.
However, I still suspect that the alignment is necessary, or logically correct?

#define ZEND_CALL_FRAME_SLOT \
	((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) \
        / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))

let's say sizeof(zend_execute_data) = 5, and sizeof(zval) = 1, then we can expect ZEND_CALL_FRAME_SLOT be 5, right?
but with the above macro, we actually get
(align(5) + align(1) - 1) / align(1) = (8 + 8 - 1) / 8 = 1?
Maybe I got something wrong, but I still don't know where I am stuck? Need someone help hack my head.

@iluuu1994
Copy link
Member

@stkeke I'm not completely sure but the alignment might be there to avoid integer division with a remainder. E.g. if sizeof(zend_execute_data) = 8 and sizeof(zval) = 3 . In that case the result would be 2, making pointer arithmetics on the zval* to skip over the call frame incorrect. With the current size the alignment doesn't make a difference (on x86 32 or 64 bit at least I think?) but this might also be platform dependent.

@nielsdos
Copy link
Member

nielsdos commented Apr 1, 2023

You do get the result 5 as expected: https://godbolt.org/z/aErqT9n7P
EDIT: and if you decrease zvals size, the result increases as expected. But maybe I'm misunderstanding something
EDIT 2: ah with 5 & 1 I do get 1 as an answer, let me see if that makes sense

@iluuu1994
Copy link
Member

iluuu1994 commented Apr 1, 2023

EDIT 2: ah with 5 & 1 I do get 1 as an answer, let me see if that makes sense

The numbers are unrealistic but I think it does make sense. If we're skipping 1 * ZEND_MM_ALIGNED_SIZE(sizeof(zval)) (8) bytes over the start of the call frame we're safely skipping the 5 bytes zend_execute_data occupies.

@nielsdos
Copy link
Member

nielsdos commented Apr 1, 2023

EDIT 2: ah with 5 & 1 I do get 1 as an answer, let me see if that makes sense

The numbers are unrealistic but I think it does make sense. If we're skipping 1 * ZEND_MM_ALIGNED_SIZE(sizeof(zval)) (8) bytes over the start of the call frame we're safely skipping the 5 bytes zend_execute_data occupies.

Hmm, looking at the macro ZEND_CALL_VAR_NUM:

#define ZEND_CALL_VAR_NUM(call, n) \
	(((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))

It does not align call, which is a call frame. So it would skip only 1*sizeof(zval) so in this case 1 byte. Right?

@stkeke
Copy link
Contributor Author

stkeke commented Apr 1, 2023

Let's still assume sizeof(zend_execute_data)=5 and sizeof(zval)=1.
I'm looking at zend_vm_calc_used_stack(), and if we get ZEND_CALL_FRAME_SLOT as 1, can we have enough space for call frame?
I suppose we should get 5 to allocate adequate space. This function is in fact what I am struggling to understand and struggling today.

static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)
{
	uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args + func->common.T;

	if (EXPECTED(ZEND_USER_CODE(func->type))) {
		used_stack += func->op_array.last_var - MIN(func->op_array.num_args, num_args);
	}
	return used_stack * sizeof(zval);
}

@iluuu1994
Copy link
Member

iluuu1994 commented Apr 1, 2023

Zvals are 16 bytes (edit: oh sorry you meant in the example), but yes it seems that we should be consistent and use alignment in both or neither place. If we make the assumption that zvals are properly aligned in many places we could add a static assert that there's no remainder without alignment.

@stkeke
Copy link
Contributor Author

stkeke commented Apr 1, 2023

@iluuu1994 @nielsdos I think I started to understand why we need to make alignment in ZEND_CALL_FRAME_SLOT definition. Let's close this PR and keep the original code. Thanks for the discussion which make me learn more and deeply.

@iluuu1994
Copy link
Member

I think it's still worth considering to remove the alignment and replace it with a static assert, since we make the assumption that ZEND_MM_ALIGNED_SIZE(sizeof(zval)) == sizeof(zval) in a few places.

@stkeke
Copy link
Contributor Author

stkeke commented Apr 1, 2023

I think it's still worth considering to remove the alignment and replace it with a static assert, since we make the assumption that ZEND_MM_ALIGNED_SIZE(sizeof(zval)) == sizeof(zval) in a few places.

Yes, that's the assumption I just realized.

@iluuu1994
Copy link
Member

diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h
index 31ed95f675..0e96e76771 100644
--- a/Zend/zend_compile.h
+++ b/Zend/zend_compile.h
@@ -606,8 +606,9 @@ struct _zend_execute_data {
 #define ZEND_CALL_NUM_ARGS(call) \
 	(call)->This.u2.num_args
 
+ZEND_STATIC_ASSERT(ZEND_MM_ALIGNED_SIZE(sizeof(zval)) == sizeof(zval), "zval must be aligned by ZEND_MM_ALIGNMENT");
 #define ZEND_CALL_FRAME_SLOT \
-	((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))
+	((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + sizeof(zval) - 1) / sizeof(zval)))
 
 #define ZEND_CALL_VAR(call, n) \
 	((zval*)(((char*)(call)) + ((int)(n))))
diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h
index 323b6269ee..0f2939ae34 100644
--- a/Zend/zend_execute.h
+++ b/Zend/zend_execute.h
@@ -195,8 +195,9 @@ struct _zend_vm_stack {
 	zend_vm_stack prev;
 };
 
+ZEND_STATIC_ASSERT(ZEND_MM_ALIGNED_SIZE(sizeof(zval)) == sizeof(zval), "zval must be aligned by ZEND_MM_ALIGNMENT");
 #define ZEND_VM_STACK_HEADER_SLOTS \
-	((ZEND_MM_ALIGNED_SIZE(sizeof(struct _zend_vm_stack)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval)))
+	((ZEND_MM_ALIGNED_SIZE(sizeof(struct _zend_vm_stack)) + sizeof(zval) - 1) / sizeof(zval))
 
 #define ZEND_VM_STACK_ELEMENTS(stack) \
 	(((zval*)(stack)) + ZEND_VM_STACK_HEADER_SLOTS)
diff --git a/Zend/zend_portability.h b/Zend/zend_portability.h
index 48e648ce4b..c098d29d51 100644
--- a/Zend/zend_portability.h
+++ b/Zend/zend_portability.h
@@ -750,4 +750,10 @@ extern "C++" {
 # define ZEND_CGG_DIAGNOSTIC_IGNORED_END
 #endif
 
+#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) /* C11 */
+# define ZEND_STATIC_ASSERT(c, m) _Static_assert((c), m)
+#else
+# define ZEND_STATIC_ASSERT(c, m)
+#endif
+
 #endif /* ZEND_PORTABILITY_H */

Something like this should be fine I think.

@stkeke
Copy link
Contributor Author

stkeke commented Apr 1, 2023

The patch is clear enough to make sure the assumption.

@iluuu1994
Copy link
Member

iluuu1994 commented Apr 1, 2023

Hmm, I think I might be wrong. On second thought, your original commit might already be correct.

#define ZEND_CALL_FRAME_SLOT \
	((int)((sizeof(zend_execute_data) + sizeof(zval) - 1) / sizeof(zval)))

The + sizeof(zval) - 1 is precisely there to make sure the division rounds up instead of down with a remainder. So I think this actually looks fine, but I'll ask Dmitry on Monday.

@stkeke
Copy link
Contributor Author

stkeke commented Apr 1, 2023

((int)((sizeof(zend_execute_data) + sizeof(zval) - 1) / sizeof(zval)))

It looks like the implementation of ceiling() function.

Copy link
Member

@dstogov dstogov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original code was correct, but ZEND_MM_ALIGNED_SIZE() is really not necessary since sizeof(zval) is already aligned.

I would change the comments to "A number of call frame slots (zvals) reserved for zend_execute_data. CV and TMP/VAR slots lay down the stack".

May be some native speaker would propose a better comment.

@stkeke
Copy link
Contributor Author

stkeke commented Apr 3, 2023

@dstogov Thanks for the approval. I count on @iluuu1994 for this PR, who has good ZEND_STATIC_ASSERT idea which is helpful for understanding the alignment and compiling safe code.

Tony Su added 2 commits April 4, 2023 07:40
Alignment is not necessary while calculating slots reserved for
zend_execute_data and _zend_vm_stack.

ZEND_STATIC_ASSERT ensures the correct alignment while code
compilation. Credit is to Ilija Tovilo.

PR: #10988

Signed-off-by: Tony Su <[email protected]>
Reviewed-by  : Ilija Tovilo
Reviewed-by  : Dmitry Stogov
Reviewed-by  : Niels Dossche
Removed the accidential backslash character (\) in
zend_compile.h file.

Signed-off-by: Tony Su <[email protected]>
@stkeke
Copy link
Contributor Author

stkeke commented Apr 3, 2023

@iluuu1994 I consolidated all the ideas and updated commit message, rebased for merge ready.

Copy link
Member

@dstogov dstogov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any problems.

@iluuu1994 iluuu1994 merged commit bf123da into php:master Apr 4, 2023
@iluuu1994
Copy link
Member

Thanks everyone!

@stkeke
Copy link
Contributor Author

stkeke commented Apr 4, 2023

@iluuu1994 @dstogov I learned something more ... Thanks.

@stkeke stkeke deleted the clean_zend_call_frame_slot branch April 4, 2023 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants